Skip to content

AI document editing in HTML, with reviewable and undoable changes - #716

Merged
urjitc merged 49 commits into
mainfrom
feat/document-ai-html-editing
Aug 2, 2026
Merged

AI document editing in HTML, with reviewable and undoable changes#716
urjitc merged 49 commits into
mainfrom
feat/document-ai-html-editing

Conversation

@urjitc

@urjitc urjitc commented Aug 1, 2026

Copy link
Copy Markdown
Member

Switches the AI's view of workspace documents from Markdown to HTML, and adds a way to see and undo what it changed.

Why

Markdown was a lossy projection: tables, math and task lists did not survive the round trip, and "change this paragraph" meant matching on text. Documents were never stored as Markdown — it was only what the model saw — so this changes the projection, not the storage. There is nothing to migrate.

What the model sees

Documents are read as HTML blocks, each top-level block carrying a data-ref. A ref is an id plus a fingerprint of that block's content, so if a human edits a paragraph between the model reading it and editing it, the edit is rejected (stale_target) rather than clobbering the change.

Edits are structural: replace, insert_before, insert_after, delete, and replace_all. Content that is not HTML — Markdown, most often — is rejected rather than flattened into a paragraph of literal source.

What the reader sees

Every turn that edited documents gets a receipt in chat: one row per document with a Lines: +N −M tally. Clicking a row opens the document and marks the changes inline — insertions underlined, deletions struck through — with the toolbar becoming Reviewing changes: Undo | Done. Review is a read-only mode, and undo asks for confirmation.

Rows persist for the life of the transcript. Whether changes can still be reviewed comes and goes — editing the document yourself closes that window — so the row keeps the record and reports its state (Edited since, Undone).

Design notes for review

  • Review is keyed to the document, not to a view. Keying it to a view instance made every "where is it now" question a lifecycle problem, and StrictMode's teardown wiped reviews as the document mounted.
  • Marks are derived, not mapped. The plugin stores the before-document and recomputes decorations against whatever is on screen, so a document that arrives late is marked correctly instead of not at all.
  • Staleness is the server's call, and is not cached. The document session compares the live document to the receipt; caching that verdict once made a reader's own later writing show up as the assistant's.
  • Only the newest receipts are kept. Undo works on the latest edit alone, so older whole-document snapshots were unusable the moment they were superseded.
  • linkedom is required: @tiptap/core's generateJSON/generateHTML throw no window object in workerd. Verified, and it stays out of the client bundle.

Verification

vp check, 218 unit tests, 32 workerd tests, and a production build all pass. A document exercising the full schema — headings, all marks, links, both list types, task lists, code blocks, tables, alignment, inline and block math — round-trips through the AI contract without loss.

Known and deliberate

  • A deleted document on an evicted Durable Object can make its status request error rather than answer not_found. The card skips deleted items client-side so it is not reachable from the UI; the server path is left alone because the obvious patch risks checkpointing an empty document over real content.
  • document-markdown.ts remains, used by search indexing and file import. Retiring it is independent of this change.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • New Features
    • AI-assisted document editing supports structured HTML changes, citations, targeted edits, and whole-document replacements.
    • Review AI changes directly in documents with highlighted additions, removals, and changed blocks.
    • Undo applied AI edits from the document toolbar.
    • AI chat displays affected documents, paths, line-change counts, and review links.
    • Added Heading 4 and improved text-style controls.
  • Improvements
    • Document reading now provides HTML block references and continuation support.
    • Improved citation resolution and rendering.
  • Bug Fixes
    • Purged workspaces and AI data remain inaccessible after deletion.

urjitc and others added 27 commits August 1, 2026 00:08
Replace Markdown edits with bounded HTML reads, structural edits, and durable Yjs receipts.

Keep workspace result counts server-controlled.
Show one review and undo surface per assistant turn while keeping edit receipts out of model output.
Documents hold text only; the schema has no image node and pasted media is
stripped, so a Yjs update cannot approach the per-value storage ceiling the
chunked manifest guarded against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the unreachable null-target branch and the sibling-receipt cache writes in
the chat actions, collapse the status message switches into records, and reuse
the shared record helpers instead of new copies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hashing the rendered HTML meant serializing every block twice on every read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The HTML switch cut the per-call batch budget to 256 KB. Keep main's limit;
the per-document chunk cap already bounds an ordinary read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Group a turn's document edits into one card with plain Review and Undo
actions, move the in-document review controls into the toolbar in place of
formatting buttons that do nothing mid-review, and restyle the marks as
tracked changes rather than tinted diff blocks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Return a block-level added/rewritten/removed tally with receipt status and
show it per document, so the card says what happened instead of only offering
actions. Make Review the primary action.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inline decorations already span block boundaries, so marking whole blocks drew
hairline rules across the page - including around blank paragraphs that had
nothing to show. Keep block decorations for text-less atoms only, and give the
inline mark enough weight and padding to read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Map review marks through edits instead of clearing them, so typing no longer
throws the reader out of review. Move the chat receipt's tally under each
document with colour-coded counts, put the icon in the header, and drop the
card border that was clipping against the message column.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hold the editor still while review is open instead of interpreting keystrokes
mid-review. Editing resumes on Done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Report the tally as lines in and out rather than block operations, and show it
as Lines +N · -M under each document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Share one undo hook between the chat receipt and the toolbar, size the review
controls like the rest of the toolbar, and quieten the receipt header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ask before reverting, from either the chat receipt or the toolbar. End review
when the document view closes rather than leaving a session pointing at a view
that no longer exists. Say plainly when a workspace outline is empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the per-row Review and Undo buttons: clicking the document opens its
changes, and undo already lives in the toolbar next to Done, where the reader
can see what they are undoing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Split the toolbar text button class into geometry and ghost colours so Done
can be a solid button of exactly the same size, and use the shared toolbar
group spacing. Drop the redundant label from the chat receipt row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The receipt sat inside the reply bubble, which is w-fit and clips overflow, so
it was only ever as wide as the text above it. Move it alongside the bubble.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ending review on unmount could wipe a review that had just been opened: the
new document unmounts the old view in the same commit. End it only when the
closing view still owns it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The overlay re-checked the editor's content against the receipt before showing
anything, which a just-opened document fails while it is still syncing, so the
review was dismissed the moment the item opened. The server already reports
content_changed from the live document; keep that as the only staleness rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review was keyed to a view instance, so it had to be opened after that view
existed and every where-is-it-now question became a lifecycle problem: under
StrictMode the teardown wiped a review the moment the document it belonged to
mounted. Key it to the document instead. Any mounted view shows the marks, and
the closing-view bookkeeping goes away with it.

Derive the marks from the stored before-document on every change rather than
computing them once and mapping them forward, so a document that arrives late
- a reopened tab still syncing, a collaborator typing - is marked correctly
instead of not at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Validation only rejected unknown elements, so content with no elements at all
passed straight through and ProseMirror flattened it into a single paragraph of
literal source. A replace_all carrying Markdown - which a model reaches for by
habit - silently replaced a whole document and reported success.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The card only rendered rows whose changes were still reviewable, so editing a
document yourself made the assistant's receipt for that turn disappear from the
transcript. A receipt records what happened; whether it can still be acted on
is a separate fact that comes and goes.

Rows now persist and carry their state - Edited since, Newer changes since,
Undone - and a reviewable row opens the changes rather than toggling them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review response carries the server's verdict on whether the document still
matches the receipt, but it was cached forever - so reopening a review after
editing the document served the old verdict and marked the reader's own writing
as the assistant's.

Stop caching it, drop the after-document from the payload since the overlay
diffs against the live editor and never read it, and keep only the most recent
receipts: undo works on the newest edit alone, so older snapshots of the whole
document were unusable the moment they were superseded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two functions implemented the same rule - keep a valid unique ref, otherwise
mint one, deduping within the pass. The only difference was letting a
replacement inherit its target's ref, which is a one-line stamp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Links were configured never to open, so a read-only viewer - who has no cursor
to place - could not follow one at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx">

<violation number="1" location="src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx:104">
P2: Clicking a document citation whose source was deleted silently does nothing because this handler discards `reveal`'s failure result. Handling the `false` result and showing the existing “This source is no longer available.” toast would give document citations the same feedback as chat citations.</violation>
</file>

<file name="src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx">

<violation number="1" location="src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx:57">
P2: Receipt rows remain a static “Edited”/line-count display and never show the required Edited since/Undone status, so users cannot tell from the transcript that a receipt is no longer reviewable without clicking each row. Deriving and rendering the latest receipt status for each row would keep the persistent transcript accurate after undo or subsequent edits.</violation>
</file>

<file name="src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts">

<violation number="1" location="src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts:29">
P2: Edits from messages using the legacy sibling-tool representation disappear from the document-change receipt. When `action` is absent here, the compatibility path has already removed the original `workspace_edit_item` part, so preserving or deriving its document-edit action in that path would keep historical review and undo available.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread.ts:175">
P2: Documents created or edited through `orchestrate` lose workspace citation links because its nested workspace tools do not receive the new resolver. Passing the same resolver through the turn-tool configuration and Code Mode tool catalog preserves citations for both direct and orchestrated operations.</violation>

<violation number="2" location="src/features/workspaces/ai/ai-thread.ts:529">
P2: A new document can persist a stale citation from an earlier chat turn because this resolver searches all transcript records even though workspace refs are turn-scoped. Limiting lookup to `activeWorkspaceReferences` keeps old refs unresolved instead of attaching outdated source metadata.</violation>
</file>

<file name="src/features/workspaces/documents/document-html-chunk.ts">

<violation number="1" location="src/features/workspaces/documents/document-html-chunk.ts:33">
P2: A fresh start read (offset 0) of an empty document now fails instead of returning an empty/tractable result. For a document with zero blocks, `document.childCount` is 0 and the guard `offset >= document.childCount` is true for `offset: 0`, so `readDocumentHtmlChunk` returns `undefined`. In `document-session.readHtmlChunk` that becomes `{ status: "invalid_offset" }`, and the reader maps a *start* mode read (which carries no cursor at all) to `{ code: "invalid_cursor", status: "failed" }`. The AI therefore cannot read an empty document it just created — workspace_read_items reports an invalid cursor on a legitimate first read, which is misleading and blocks create-then-edit workflows on empty documents. The test case "rejects a nonzero continuation offset for an empty document" implies offset 0 should be a valid start read, but no path produces a ready result for it.</violation>
</file>

<file name="src/features/workspaces/documents/document-ai-edits.ts">

<violation number="1" location="src/features/workspaces/documents/document-ai-edits.ts:181">
P3: Line tallies for atom blocks (rules, formulas, etc.) can be miscounted across whole-document rewrites. `countDocumentLines` keys atom lines by the full node JSON, which includes the internal `aiRef` attribute that `ensureProseMirrorDocumentAiRefs` regenerates on every top-level block during `replace_all`. As a result an atom whose visible content did not change is still reported as one line removed and one line added in the receipt tally. Consider stripping the ref (e.g. `withTiptapNodeAiRef(node, null)` before `toJSON()`, or key on the block's content) so the tally reflects visible content only, matching how text blocks are keyed by `textContent`.</violation>
</file>

<file name="src/features/workspaces/documents/tiptap-schema.ts">

<violation number="1" location="src/features/workspaces/documents/tiptap-schema.ts:121">
P3: This schema is shared with the live client editor (`getTiptapDocumentSchemaExtensions` is spread into the editor via `tiptap-extensions.ts`), so flipping `Link` `openOnClick` from `false` to `true` changes link interaction for every document being edited, not just the server-side HTML projection used for AI editing. Clicking a link in the editor now navigates to the URL instead of keeping the cursor/selection, a user-facing behavior change in normal editing. `openOnClick` is an editor-interaction option with no effect on server parse/serialize, so unless this editor UX change is intentional for the PR it reads as an incidental side effect of the schema refactor.</violation>
</file>

<file name="src/features/workspaces/documents/document-session.ts">

<violation number="1" location="src/features/workspaces/documents/document-session.ts:444">
P2: The undo/edit write path changed how the shared document fragment is replaced. Previously `replaceCurrentDocument` explicitly cleared the Y.XmlFragment (`fragment.delete(0, fragment.length)`) before calling `prosemirrorJSONToYXmlFragment`; the new `reconcileCurrentDocument` calls it directly on the existing fragment with no clear. If `prosemirrorJSONToYXmlFragment` merges into rather than replaces the fragment content, every applied edit, undo, and ref-reconciliation in `applyEdits`/`undoDocumentEditReceipt`/`getReferencedDocumentSnapshot` would append the full document again, compounding content. Please confirm the helper replaces its target fragment; if it does not, restore the `fragment.delete(0, fragment.length)` (and the surrounding transaction) before inserting.</violation>
</file>

<file name="src/features/workspaces/operations/document-citations.ts">

<violation number="1" location="src/features/workspaces/operations/document-citations.ts:14">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `resolveDocumentCitations` operation is introduced without unit tests, even though regression-style assertions are practical. The function has two distinct branches—no-op when refs are empty or the resolver is absent, and mapping resolved records back into HTML—that could be trivially covered with a mocked `WorkspaceAccessContext`. In a behavior-change PR, introducing untested core logic increases the risk of silent regressions in citation resolution. Adding a focused test file (e.g., `document-citations.test.ts`) would exercise both branches and document the expected behavior for future changes.</violation>
</file>

<file name="src/features/workspaces/documents/use-document-collaboration-session.ts">

<violation number="1" location="src/features/workspaces/documents/use-document-collaboration-session.ts:209">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new local-persistence readiness condition that checks `session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0` before marking a cached session ready is a behavior change with no test coverage. A practical regression assertion exists (seed local persistence with the `server-synced` flag and an empty tiptap fragment, assert the session is not marked ready), yet no tests cover this path. For a behavior-change PR, new gating logic should be validated by a regression test.</violation>
</file>

<file name="src/features/workspaces/components/document-editor/DocumentToolbar.tsx">

<violation number="1" location="src/features/workspaces/components/document-editor/DocumentToolbar.tsx:55">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

This behavior-change PR introduces new conditional render paths in DocumentToolbar (review mode, read-only mode, and a font-size→text-style rename) that are not exercised by any visible tests. Rule 1 flags behavior-change PRs where changed behavior is not tested, especially when regression-style assertions are practical. Consider adding component-level tests that verify the review controls render when activeReview.itemId matches, the minimal toolbar renders when canEdit is false, and the renamed text-style actions and icons behave correctly.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread-orchestration.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread-orchestration.ts:133">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new behavior branches for model output transformation and edit-receipt metadata attachment are not covered by regression-style tests. `attachDocumentEditReceiptMetadata` has no test references at all in the test suite, and while `getAIThreadOrchestrationModelOutput` is tested as a standalone function, the `toModelOutput` property added to the tool returned by `createAIThreadOrchestrationTool` is never exercised. Since the existing worker test already mocks `createExecuteRuntime` and exercises connector tools, practical assertions could be added for both branches: one verifying that `workspace_edit_item` results carry the edit-receipt metadata, and another verifying the tool's `toModelOutput` produces the expected model-visible JSON shape.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


const page = Number(citation?.getAttribute("data-page"));
event.preventDefault();
reveal(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Clicking a document citation whose source was deleted silently does nothing because this handler discards reveal's failure result. Handling the false result and showing the existing “This source is no longer available.” toast would give document citations the same feedback as chat citations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx, line 104:

<comment>Clicking a document citation whose source was deleted silently does nothing because this handler discards `reveal`'s failure result. Handling the `false` result and showing the existing “This source is no longer available.” toast would give document citations the same feedback as chat citations.</comment>

<file context>
@@ -85,6 +89,26 @@ function DocumentEditorInstance({
+
+					const page = Number(citation?.getAttribute("data-page"));
+					event.preventDefault();
+					reveal(
+						Number.isInteger(page) && page > 0
+							? { itemId, kind: "pdf-page", pageNumber: page, version: 1 }
</file context>

// document or switching tabs as needed. If they are no longer reviewable the
// document still opens and the overlay says why.
return (
<button

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Receipt rows remain a static “Edited”/line-count display and never show the required Edited since/Undone status, so users cannot tell from the transcript that a receipt is no longer reviewable without clicking each row. Deriving and rendering the latest receipt status for each row would keep the persistent transcript accurate after undo or subsequent edits.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx, line 57:

<comment>Receipt rows remain a static “Edited”/line-count display and never show the required Edited since/Undone status, so users cannot tell from the transcript that a receipt is no longer reviewable without clicking each row. Deriving and rendering the latest receipt status for each row would keep the persistent transcript accurate after undo or subsequent edits.</comment>

<file context>
@@ -0,0 +1,107 @@
+	// document or switching tabs as needed. If they are no longer reviewable the
+	// document still opens and the overlay says why.
+	return (
+		<button
+			type="button"
+			className="flex w-full min-w-0 items-center gap-2 px-2.5 py-2 text-left transition-colors hover:bg-foreground/5"
</file context>

for (const part of parts) {
if (isAiChatToolGroupPart(part)) {
for (const child of part.children) {
if (child.status === "completed" && child.action?.kind === "document-edit") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Edits from messages using the legacy sibling-tool representation disappear from the document-change receipt. When action is absent here, the compatibility path has already removed the original workspace_edit_item part, so preserving or deriving its document-edit action in that path would keep historical review and undo available.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts, line 29:

<comment>Edits from messages using the legacy sibling-tool representation disappear from the document-change receipt. When `action` is absent here, the compatibility path has already removed the original `workspace_edit_item` part, so preserving or deriving its document-edit action in that path would keep historical review and undo available.</comment>

<file context>
@@ -0,0 +1,100 @@
+	for (const part of parts) {
+		if (isAiChatToolGroupPart(part)) {
+			for (const child of part.children) {
+				if (child.status === "completed" && child.action?.kind === "document-edit") {
+					addToGroup(groupsByItemId, seenReceiptIds, child.action);
+				}
</file context>

onWorkspaceReferences: (records) => {
this._recordWorkspaceReferences(records);
},
resolveWorkspaceReferences: (refs) => this._resolveWorkspaceReferences(refs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Documents created or edited through orchestrate lose workspace citation links because its nested workspace tools do not receive the new resolver. Passing the same resolver through the turn-tool configuration and Code Mode tool catalog preserves citations for both direct and orchestrated operations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/ai-thread.ts, line 175:

<comment>Documents created or edited through `orchestrate` lose workspace citation links because its nested workspace tools do not receive the new resolver. Passing the same resolver through the turn-tool configuration and Code Mode tool catalog preserves citations for both direct and orchestrated operations.</comment>

<file context>
@@ -172,6 +172,7 @@ export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
 				onWorkspaceReferences: (records) => {
 					this._recordWorkspaceReferences(records);
 				},
+				resolveWorkspaceReferences: (refs) => this._resolveWorkspaceReferences(refs),
 			});
 		}
</file context>

private async _resolveWorkspaceReferences(refs: readonly string[]) {
const wanted = new Set(refs);
const records = [
...collectWorkspaceReferenceRecords(await this.getMessages()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A new document can persist a stale citation from an earlier chat turn because this resolver searches all transcript records even though workspace refs are turn-scoped. Limiting lookup to activeWorkspaceReferences keeps old refs unresolved instead of attaching outdated source metadata.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/ai-thread.ts, line 529:

<comment>A new document can persist a stale citation from an earlier chat turn because this resolver searches all transcript records even though workspace refs are turn-scoped. Limiting lookup to `activeWorkspaceReferences` keeps old refs unresolved instead of attaching outdated source metadata.</comment>

<file context>
@@ -517,6 +518,21 @@ export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
+		private async _resolveWorkspaceReferences(refs: readonly string[]) {
+			const wanted = new Set(refs);
+			const records = [
+				...collectWorkspaceReferenceRecords(await this.getMessages()),
+				...this.activeWorkspaceReferences,
+			];
</file context>

if (wasServerSynced === localDocumentReadyValue) {
if (
wasServerSynced === localDocumentReadyValue &&
session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new local-persistence readiness condition that checks session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0 before marking a cached session ready is a behavior change with no test coverage. A practical regression assertion exists (seed local persistence with the server-synced flag and an empty tiptap fragment, assert the session is not marked ready), yet no tests cover this path. For a behavior-change PR, new gating logic should be validated by a regression test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/use-document-collaboration-session.ts, line 209:

<comment>The new local-persistence readiness condition that checks `session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0` before marking a cached session ready is a behavior change with no test coverage. A practical regression assertion exists (seed local persistence with the `server-synced` flag and an empty tiptap fragment, assert the session is not marked ready), yet no tests cover this path. For a behavior-change PR, new gating logic should be validated by a regression test.</comment>

<file context>
@@ -203,7 +204,10 @@ function createActiveDocumentSession(input: {
-			if (wasServerSynced === localDocumentReadyValue) {
+			if (
+				wasServerSynced === localDocumentReadyValue &&
+				session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0
+			) {
 				markReady();
</file context>


// Reviewing borrows the toolbar rather than floating over the page: the
// formatting controls are unusable mid-review anyway, so the space is free.
if (activeReview?.itemId === itemId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

This behavior-change PR introduces new conditional render paths in DocumentToolbar (review mode, read-only mode, and a font-size→text-style rename) that are not exercised by any visible tests. Rule 1 flags behavior-change PRs where changed behavior is not tested, especially when regression-style assertions are practical. Consider adding component-level tests that verify the review controls render when activeReview.itemId matches, the minimal toolbar renders when canEdit is false, and the renamed text-style actions and icons behave correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/document-editor/DocumentToolbar.tsx, line 55:

<comment>This behavior-change PR introduces new conditional render paths in DocumentToolbar (review mode, read-only mode, and a font-size→text-style rename) that are not exercised by any visible tests. Rule 1 flags behavior-change PRs where changed behavior is not tested, especially when regression-style assertions are practical. Consider adding component-level tests that verify the review controls render when activeReview.itemId matches, the minimal toolbar renders when canEdit is false, and the renamed text-style actions and icons behave correctly.</comment>

<file context>
@@ -33,9 +36,45 @@ import {
+
+	// Reviewing borrows the toolbar rather than floating over the page: the
+	// formatting controls are unusable mid-review anyway, so the space is free.
+	if (activeReview?.itemId === itemId) {
+		return (
+			<DocumentEditReviewControls
</file context>

source: "codemode",
});

return toolName === "workspace_edit_item"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new behavior branches for model output transformation and edit-receipt metadata attachment are not covered by regression-style tests. attachDocumentEditReceiptMetadata has no test references at all in the test suite, and while getAIThreadOrchestrationModelOutput is tested as a standalone function, the toModelOutput property added to the tool returned by createAIThreadOrchestrationTool is never exercised. Since the existing worker test already mocks createExecuteRuntime and exercises connector tools, practical assertions could be added for both branches: one verifying that workspace_edit_item results carry the edit-receipt metadata, and another verifying the tool's toModelOutput produces the expected model-visible JSON shape.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/ai-thread-orchestration.ts, line 133:

<comment>The new behavior branches for model output transformation and edit-receipt metadata attachment are not covered by regression-style tests. `attachDocumentEditReceiptMetadata` has no test references at all in the test suite, and while `getAIThreadOrchestrationModelOutput` is tested as a standalone function, the `toModelOutput` property added to the tool returned by `createAIThreadOrchestrationTool` is never exercised. Since the existing worker test already mocks `createExecuteRuntime` and exercises connector tools, practical assertions could be added for both branches: one verifying that `workspace_edit_item` results carry the edit-receipt metadata, and another verifying the tool's `toModelOutput` produces the expected model-visible JSON shape.</comment>

<file context>
@@ -114,11 +123,16 @@ class AIThreadToolSetConnector extends CodemodeConnector {
 								source: "codemode",
 							});
+
+							return toolName === "workspace_edit_item"
+								? attachDocumentEditReceiptMetadata(output, invocationId)
+								: output;
</file context>

}
// A rule or a formula holds no text but still occupies a line.
if (node.isAtom) {
countLine(JSON.stringify(node.toJSON()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Line tallies for atom blocks (rules, formulas, etc.) can be miscounted across whole-document rewrites. countDocumentLines keys atom lines by the full node JSON, which includes the internal aiRef attribute that ensureProseMirrorDocumentAiRefs regenerates on every top-level block during replace_all. As a result an atom whose visible content did not change is still reported as one line removed and one line added in the receipt tally. Consider stripping the ref (e.g. withTiptapNodeAiRef(node, null) before toJSON(), or key on the block's content) so the tally reflects visible content only, matching how text blocks are keyed by textContent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-ai-edits.ts, line 181:

<comment>Line tallies for atom blocks (rules, formulas, etc.) can be miscounted across whole-document rewrites. `countDocumentLines` keys atom lines by the full node JSON, which includes the internal `aiRef` attribute that `ensureProseMirrorDocumentAiRefs` regenerates on every top-level block during `replace_all`. As a result an atom whose visible content did not change is still reported as one line removed and one line added in the receipt tally. Consider stripping the ref (e.g. `withTiptapNodeAiRef(node, null)` before `toJSON()`, or key on the block's content) so the tally reflects visible content only, matching how text blocks are keyed by `textContent`.</comment>

<file context>
@@ -0,0 +1,312 @@
+			}
+			// A rule or a formula holds no text but still occupies a line.
+			if (node.isAtom) {
+				countLine(JSON.stringify(node.toJSON()));
+				return false;
+			}
</file context>

Highlight,
Link.configure({
openOnClick: false,
openOnClick: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This schema is shared with the live client editor (getTiptapDocumentSchemaExtensions is spread into the editor via tiptap-extensions.ts), so flipping Link openOnClick from false to true changes link interaction for every document being edited, not just the server-side HTML projection used for AI editing. Clicking a link in the editor now navigates to the URL instead of keeping the cursor/selection, a user-facing behavior change in normal editing. openOnClick is an editor-interaction option with no effect on server parse/serialize, so unless this editor UX change is intentional for the PR it reads as an incidental side effect of the schema refactor.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/tiptap-schema.ts, line 121:

<comment>This schema is shared with the live client editor (`getTiptapDocumentSchemaExtensions` is spread into the editor via `tiptap-extensions.ts`), so flipping `Link` `openOnClick` from `false` to `true` changes link interaction for every document being edited, not just the server-side HTML projection used for AI editing. Clicking a link in the editor now navigates to the URL instead of keeping the cursor/selection, a user-facing behavior change in normal editing. `openOnClick` is an editor-interaction option with no effect on server parse/serialize, so unless this editor UX change is intentional for the PR it reads as an incidental side effect of the schema refactor.</comment>

<file context>
@@ -40,7 +118,7 @@ export function getTiptapDocumentSchemaExtensions({
 		Highlight,
 		Link.configure({
-			openOnClick: false,
+			openOnClick: true,
 			autolink: true,
 			defaultProtocol: "https",
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/operations/create-items.ts">

<violation number="1" location="src/features/workspaces/operations/create-items.ts:121">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

This behavior-change PR modifies `createWorkspaceItemsOperation` to resolve citations via `resolveDocumentCitations` and parse initial content with `parseDocumentAiHtml`, yet no tests exercise the new integration or the updated failure path. A regression-style test for document creation with HTML/citations—and for the `invalid_initial_content` rejection—should be added to guard against regressions.</violation>
</file>

<file name="src/features/workspaces/content/workspace-content-reader.ts">

<violation number="1" location="src/features/workspaces/content/workspace-content-reader.ts:50">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

The `readBudgetExhausted` flag changes batch read semantics so that once any item exceeds the byte budget, all later items are rejected without being read. Previously each item was checked independently, so a small item after a skipped large one could still be returned. This behavior change is not documented in the PR description or in code comments, and the existing test `bounds total content returned by a batch` cannot distinguish the new semantics from the old because every test item is identical. A regression-style assertion should be added (or the semantics should be reverted and documented) so that mixed-sized batches are handled correctly and items that would have returned real read errors are not masked as `read_budget_exceeded`.</violation>
</file>

<file name="src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx">

<violation number="1" location="src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx:104">
P2: Clicking an inline citation whose source was deleted or is otherwise unavailable silently does nothing. Handle the `reveal` return value and surface the same unavailable-source feedback used by chat citations.</violation>
</file>

<file name="src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts">

<violation number="1" location="src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts:27">
P2: Document-edit receipts disappear for legacy assistant messages that contain a Code Mode part plus a sibling `workspace_edit_item`: legacy display grouping drops the sibling's document-edit action, and this branch skips the original part. Preserving the action when building legacy children, or retaining/processing those sibling tool parts here, keeps review and undo available for those messages.</violation>
</file>

<file name="src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx">

<violation number="1" location="src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx:61">
P2: Responses with more than eight successful edits to one document cannot be reviewed or undone, even though the latest document is valid. Pass only a supported contiguous receipt suffix or change the grouping/retention contract so evicted IDs are not sent.</violation>

<violation number="2" location="src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx:70">
P2: After Undo, the transcript receipt still looks like an active edit and offers no `Undone` or `Edited since` state. Add live receipt-status state or update the row when review and undo complete.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread-orchestration-contract.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread-orchestration-contract.ts:147">
P2: The Code Mode model can read the document-edit receipt ID even though this projection is intended to keep UI metadata app-only. Stripping only the final result is too late; the connector should remove UI metadata before returning the nested tool result or carry the receipt through a non-model-visible side channel.</violation>
</file>

<file name="src/features/workspaces/documents/tiptap-schema.ts">

<violation number="1" location="src/features/workspaces/documents/tiptap-schema.ts:121">
P2: This flips link behavior for the entire collaborative editor from `openOnClick: false` to `true`, so clicking any link in a document now opens it in a new tab rather than letting the user click to position the cursor and edit it. That is a cross-cutting change to every document edit surface, while the PR's feature is about the read-only review/undo workflow where clickable links make sense. If the intent was only to make links open in the review window, scope the behavior there; otherwise confirm this global editor change is intended.</violation>
</file>

<file name="src/features/workspaces/documents/use-document-edit-review-overlay.ts">

<violation number="1" location="src/features/workspaces/documents/use-document-edit-review-overlay.ts:45">
P1: If loading the review fails or the server reports the changes are no longer reviewable, the document editor is left permanently read-only. The effect freezes the editor with `setEditable(false)` up front, but the `isError` and `status !== "ready"` branches return early without scheduling any cleanup, and the only `setEditable(canEdit)` restore lives in the success-path cleanup. When `hideReview()` clears the active review, React re-runs the effect with `target === null`, and since no cleanup was registered the editor stays frozen. Recommend restoring editability on the failure paths (for example, call `editor.setEditable(canEdit)` before `hideReview()` in both early-return branches, or always return a cleanup that resets editability).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// Freeze on request rather than on arrival: the verdict takes a round trip,
// and a document that accepts typing in the meantime is one whose marks
// will not match it.
editor.setEditable(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: If loading the review fails or the server reports the changes are no longer reviewable, the document editor is left permanently read-only. The effect freezes the editor with setEditable(false) up front, but the isError and status !== "ready" branches return early without scheduling any cleanup, and the only setEditable(canEdit) restore lives in the success-path cleanup. When hideReview() clears the active review, React re-runs the effect with target === null, and since no cleanup was registered the editor stays frozen. Recommend restoring editability on the failure paths (for example, call editor.setEditable(canEdit) before hideReview() in both early-return branches, or always return a cleanup that resets editability).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/use-document-edit-review-overlay.ts, line 45:

<comment>If loading the review fails or the server reports the changes are no longer reviewable, the document editor is left permanently read-only. The effect freezes the editor with `setEditable(false)` up front, but the `isError` and `status !== "ready"` branches return early without scheduling any cleanup, and the only `setEditable(canEdit)` restore lives in the success-path cleanup. When `hideReview()` clears the active review, React re-runs the effect with `target === null`, and since no cleanup was registered the editor stays frozen. Recommend restoring editability on the failure paths (for example, call `editor.setEditable(canEdit)` before `hideReview()` in both early-return branches, or always return a cleanup that resets editability).</comment>

<file context>
@@ -0,0 +1,94 @@
+		// Freeze on request rather than on arrival: the verdict takes a round trip,
+		// and a document that accepts typing in the meantime is one whose marks
+		// will not match it.
+		editor.setEditable(false);
+
+		if (reviewQuery.isError) {
</file context>

const results: WorkspaceContentReadResult[] = [];
const readyResults: PendingReadyResult[] = [];
let returnedContentBytes = 0;
let readBudgetExhausted = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag AI Slop and Fabricated Changes

The readBudgetExhausted flag changes batch read semantics so that once any item exceeds the byte budget, all later items are rejected without being read. Previously each item was checked independently, so a small item after a skipped large one could still be returned. This behavior change is not documented in the PR description or in code comments, and the existing test bounds total content returned by a batch cannot distinguish the new semantics from the old because every test item is identical. A regression-style assertion should be added (or the semantics should be reverted and documented) so that mixed-sized batches are handled correctly and items that would have returned real read errors are not masked as read_budget_exceeded.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/content/workspace-content-reader.ts, line 50:

<comment>The `readBudgetExhausted` flag changes batch read semantics so that once any item exceeds the byte budget, all later items are rejected without being read. Previously each item was checked independently, so a small item after a skipped large one could still be returned. This behavior change is not documented in the PR description or in code comments, and the existing test `bounds total content returned by a batch` cannot distinguish the new semantics from the old because every test item is identical. A regression-style assertion should be added (or the semantics should be reverted and documented) so that mixed-sized batches are handled correctly and items that would have returned real read errors are not masked as `read_budget_exceeded`.</comment>

<file context>
@@ -49,6 +47,7 @@ export async function readWorkspaceContent(input: {
 	const results: WorkspaceContentReadResult[] = [];
 	const readyResults: PendingReadyResult[] = [];
 	let returnedContentBytes = 0;
+	let readBudgetExhausted = false;
 
 	// Reads stay ordered so each body is consumed before the shared byte budget advances.
</file context>


const page = Number(citation?.getAttribute("data-page"));
event.preventDefault();
reveal(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Clicking an inline citation whose source was deleted or is otherwise unavailable silently does nothing. Handle the reveal return value and surface the same unavailable-source feedback used by chat citations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx, line 104:

<comment>Clicking an inline citation whose source was deleted or is otherwise unavailable silently does nothing. Handle the `reveal` return value and surface the same unavailable-source feedback used by chat citations.</comment>

<file context>
@@ -85,6 +89,26 @@ function DocumentEditorInstance({
+
+					const page = Number(citation?.getAttribute("data-page"));
+					event.preventDefault();
+					reveal(
+						Number.isInteger(page) && page > 0
+							? { itemId, kind: "pdf-page", pageNumber: page, version: 1 }
</file context>

const seenReceiptIds = new Set<string>();

for (const part of parts) {
if (isAiChatToolGroupPart(part)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Document-edit receipts disappear for legacy assistant messages that contain a Code Mode part plus a sibling workspace_edit_item: legacy display grouping drops the sibling's document-edit action, and this branch skips the original part. Preserving the action when building legacy children, or retaining/processing those sibling tool parts here, keeps review and undo available for those messages.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/ai-chat-document-edit-actions.ts, line 27:

<comment>Document-edit receipts disappear for legacy assistant messages that contain a Code Mode part plus a sibling `workspace_edit_item`: legacy display grouping drops the sibling's document-edit action, and this branch skips the original part. Preserving the action when building legacy children, or retaining/processing those sibling tool parts here, keeps review and undo available for those messages.</comment>

<file context>
@@ -0,0 +1,100 @@
+	const seenReceiptIds = new Set<string>();
+
+	for (const part of parts) {
+		if (isAiChatToolGroupPart(part)) {
+			for (const child of part.children) {
+				if (child.status === "completed" && child.action?.kind === "document-edit") {
</file context>

<div className="truncate text-sm" title={group.path}>
<DocumentPathLabel path={group.path} />
</div>
<ChangeSummary lineChanges={group.lineChanges} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: After Undo, the transcript receipt still looks like an active edit and offers no Undone or Edited since state. Add live receipt-status state or update the row when review and undo complete.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx, line 70:

<comment>After Undo, the transcript receipt still looks like an active edit and offers no `Undone` or `Edited since` state. Add live receipt-status state or update the row when review and undo complete.</comment>

<file context>
@@ -0,0 +1,107 @@
+				<div className="truncate text-sm" title={group.path}>
+					<DocumentPathLabel path={group.path} />
+				</div>
+				<ChangeSummary lineChanges={group.lineChanges} />
+			</div>
+		</button>
</file context>

type="button"
className="flex w-full min-w-0 items-center gap-2 px-2.5 py-2 text-left transition-colors hover:bg-foreground/5"
onClick={() => {
if (!showReview({ itemId: group.itemId, receiptIds: group.receiptIds })) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Responses with more than eight successful edits to one document cannot be reviewed or undone, even though the latest document is valid. Pass only a supported contiguous receipt suffix or change the grouping/retention contract so evicted IDs are not sent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx, line 61:

<comment>Responses with more than eight successful edits to one document cannot be reviewed or undone, even though the latest document is valid. Pass only a supported contiguous receipt suffix or change the grouping/retention contract so evicted IDs are not sent.</comment>

<file context>
@@ -0,0 +1,107 @@
+			type="button"
+			className="flex w-full min-w-0 items-center gap-2 px-2.5 py-2 text-left transition-colors hover:bg-foreground/5"
+			onClick={() => {
+				if (!showReview({ itemId: group.itemId, receiptIds: group.receiptIds })) {
+					toast.error("This document no longer exists.");
+				}
</file context>

status: parsed.data.status,
executionId: parsed.data.executionId,
result: parsed.data.result,
result: stripAIThreadToolUiMetadata(parsed.data.result),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Code Mode model can read the document-edit receipt ID even though this projection is intended to keep UI metadata app-only. Stripping only the final result is too late; the connector should remove UI metadata before returning the nested tool result or carry the receipt through a non-model-visible side channel.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/ai-thread-orchestration-contract.ts, line 147:

<comment>The Code Mode model can read the document-edit receipt ID even though this projection is intended to keep UI metadata app-only. Stripping only the final result is too late; the connector should remove UI metadata before returning the nested tool result or carry the receipt through a non-model-visible side channel.</comment>

<file context>
@@ -130,7 +144,7 @@ export function normalizeAIThreadOrchestrationOutput(output: unknown): AIThreadO
 			status: parsed.data.status,
 			executionId: parsed.data.executionId,
-			result: parsed.data.result,
+			result: stripAIThreadToolUiMetadata(parsed.data.result),
 			calls,
 			outcome: childOutcome,
</file context>

Comment thread src/features/workspaces/documents/document-ai-html.ts
Highlight,
Link.configure({
openOnClick: false,
openOnClick: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This flips link behavior for the entire collaborative editor from openOnClick: false to true, so clicking any link in a document now opens it in a new tab rather than letting the user click to position the cursor and edit it. That is a cross-cutting change to every document edit surface, while the PR's feature is about the read-only review/undo workflow where clickable links make sense. If the intent was only to make links open in the review window, scope the behavior there; otherwise confirm this global editor change is intended.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/tiptap-schema.ts, line 121:

<comment>This flips link behavior for the entire collaborative editor from `openOnClick: false` to `true`, so clicking any link in a document now opens it in a new tab rather than letting the user click to position the cursor and edit it. That is a cross-cutting change to every document edit surface, while the PR's feature is about the read-only review/undo workflow where clickable links make sense. If the intent was only to make links open in the review window, scope the behavior there; otherwise confirm this global editor change is intended.</comment>

<file context>
@@ -40,7 +118,7 @@ export function getTiptapDocumentSchemaExtensions({
 		Highlight,
 		Link.configure({
-			openOnClick: false,
+			openOnClick: true,
 			autolink: true,
 			defaultProtocol: "https",
</file context>
Suggested change
openOnClick: true,
openOnClick: false,

}

const initialContent = getCreateWorkspaceItemInitialContent(itemInput);
const initialContent = getCreateWorkspaceItemInitialContent(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

This behavior-change PR modifies createWorkspaceItemsOperation to resolve citations via resolveDocumentCitations and parse initial content with parseDocumentAiHtml, yet no tests exercise the new integration or the updated failure path. A regression-style test for document creation with HTML/citations—and for the invalid_initial_content rejection—should be added to guard against regressions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/operations/create-items.ts, line 121:

<comment>This behavior-change PR modifies `createWorkspaceItemsOperation` to resolve citations via `resolveDocumentCitations` and parse initial content with `parseDocumentAiHtml`, yet no tests exercise the new integration or the updated failure path. A regression-style test for document creation with HTML/citations—and for the `invalid_initial_content` rejection—should be added to guard against regressions.</comment>

<file context>
@@ -118,7 +118,17 @@ export async function createWorkspaceItemsOperation(
 		}
 
-		const initialContent = getCreateWorkspaceItemInitialContent(itemInput);
+		const initialContent = getCreateWorkspaceItemInitialContent(
+			itemInput.type === "document" && itemInput.initialContent !== undefined
+				? {
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 issues found across 59 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/document-editor/document-editor-state.ts">

<violation number="1" location="src/features/workspaces/components/document-editor/document-editor-state.ts:169">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

This behavior change adds heading level 4 detection and renames the state union from `DocumentFontSize` to `DocumentTextStyle`, but no tests in the suite exercise `getActiveBlock` or the new `textStyle` values. A practical regression test (for example, mocking a TipTap editor and asserting the returned block for each heading level and the paragraph default) would prevent silent breakage of the toolbar state. Please add unit tests that cover the new `heading4` branch and the semantic rename.</violation>
</file>

<file name="src/features/workspaces/components/document-editor/document-editor-toolbar-actions.tsx">

<violation number="1" location="src/features/workspaces/components/document-editor/document-editor-toolbar-actions.tsx:80">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The toolbar semantics changed from font-size-based actions to text-style-based actions (paragraph and heading levels 1–4), including a new Heading 4 action with its own `active` state, `run` command, and icon mapping. However, there is no visible test coverage exercising this new behavior. Consider adding unit tests that assert the `isTextStyle` active-state logic, the `getTextStyleIcon` mappings, and the TipTap commands invoked by each `documentTextStyleActions` entry—including the new heading level 4—to prevent regressions in the document editor toolbar.</violation>
</file>

<file name="src/features/workspaces/documents/use-document-collaboration-session.ts">

<violation number="1" location="src/features/workspaces/documents/use-document-collaboration-session.ts:207">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The modified persistence-based readiness condition introduces a real behavior change, but there's no visible test coverage for it. A session that has the cached 'server-synced' marker in IndexedDB but an empty tiptap XML fragment will no longer be marked ready early and will instead wait for the websocket sync event. This edge case is now silently altered without regression protection. Consider adding a test that mocks the IndexedDB marker and toggles an empty vs. non-empty `Y.XmlFragment` to assert whether `markReady()` fires without a websocket sync — this directly exercises the change and guards against regressions.</violation>
</file>

<file name="src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx">

<violation number="1" location="src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx:92">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

This PR adds new user-visible citation-click navigation behavior, but no corresponding tests were found that exercise this interaction. Rule 1 (Flag AI Slop and Fabricated Changes) specifically calls out behavior-change PRs where changed behavior is not exercised by tests, especially when a regression-style assertion is practical. The click handler extracts `data-item-id` and `data-page`, parses the page conditionally, and calls `reveal()` — all of which is testable. Similarly, the newly wired `useDocumentEditReviewOverlay` hook lacks test coverage. Consider adding tests that verify the navigation logic and overlay integration to prevent regressions.</violation>

<violation number="2" location="src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx:103">
P3: Clicking a citation that points at an item that has since been deleted or renamed silently does nothing. The new click handler always calls event.preventDefault() and returns true as if the citation was actioned, but it discards the boolean reveal() returns (workspace-location-context.tsx returns Boolean(viewInstanceId), which is false when the target item no longer exists). Because the new styles.css gives citations cursor:pointer, a stale citation still looks clickable but produces no navigation and no feedback. Consider checking reveal()'s result (or hasItem) and, when it returns false, falling back to opening the plain document item or otherwise signalling that the source is unavailable, rather than swallowing the click.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread-orchestration.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread-orchestration.ts:80">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

This behavior-change PR adds new orchestration wiring (`toModelOutput` on the orchestration tool and receipt-metadata attachment for `workspace_edit_item`) without tests that exercise these exact code paths. The related output-normalization helpers are tested, but the wiring that connects them into the tool lifecycle is not. A regression-style test should assert that `createAIThreadOrchestrationTool` returns a tool whose `toModelOutput` produces the expected compact projection, and that a `workspace_edit_item` invocation through the connector carries receipt metadata keyed by `invocationId`.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread.ts:175">
P1: Document citations are lost when the assistant edits through `orchestrate`: the resolver is wired into direct workspace tools here, but not into the separately constructed Code Mode workspace tools. Thread the resolver through `createAIThreadTurnToolConfig` and its tool catalog as well so both execution paths resolve `<citation ref>` locations.</violation>
</file>

<file name="src/features/workspaces/operations/edit-item.ts">

<violation number="1" location="src/features/workspaces/operations/edit-item.ts:84">
P2: A batch with multiple citation-bearing HTML edits now rereads the chat transcript once per edit, creating up to 40 concurrent Durable Object reads for one tool call and avoidable latency/load. Resolving the union of citation refs once and reusing the records for each HTML fragment would preserve behavior without the per-edit read amplification.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread-orchestration-contract.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread-orchestration-contract.ts:208">
P3: The model-facing boundary strips app-only `action` from calls but does not strip the `__thinkexUi`/receipt metadata from `result` — it only stays clean because `execute` happens to feed it already-normalized (pre-stripped) output. If this exported helper is ever called with the raw runtime output (its signature accepts any `output`), the internal receipt ID would surface to the model. Applying `stripAIThreadToolUiMetadata` to `result` here (as `normalizeAIThreadOrchestrationOutput` already does) would make the boundary self-contained and consistent.</violation>
</file>

<file name="src/features/workspaces/documents/document-session.ts">

<violation number="1" location="src/features/workspaces/documents/document-session.ts:443">
P2: `reconcileCurrentDocument` now repopulates an existing Yjs `XmlFragment` without clearing it first, whereas the previous `replaceCurrentDocument` explicitly did `fragment.delete(0, fragment.length)` and wrapped the write in `this.document.transact(...)`. If `prosemirrorJSONToYXmlFragment` appends (as the old code's explicit delete strongly suggests), every AI edit and every ref reconciliation would duplicate the document's top-level blocks, and dropping the `transact` wrapper also removes the atomicity the old code guaranteed when streaming updates to connected clients. Please verify the library clears the fragment and restore the explicit clear/transaction (as in the removed code) if it does not.</violation>
</file>

<file name="src/features/workspaces/documents/document-ai-html.test.ts">

<violation number="1" location="src/features/workspaces/documents/document-ai-html.test.ts:23">
P3: The round-trip test never verifies the round-trip: it only checks substrings and that re-parsing yields `type: "doc"`, so silently dropped or reordered blocks still pass despite the test name and the PR's "remains lossless" claim. Consider asserting the re-parsed document deep-equals the original (or a normalized snapshot) to actually guard the round-trip.</violation>
</file>

<file name="src/features/workspaces/operations/document-citations.ts">

<violation number="1" location="src/features/workspaces/operations/document-citations.ts:26">
P2: Model-supplied citation metadata can survive resolution: an unresolved ref can retain a fabricated `data-item-id`, and an item citation can retain a fabricated `data-page`. Strip server-owned citation attributes from ref-bearing tags before applying locations so only a resolved workspace record supplies them.</violation>

<violation number="2" location="src/features/workspaces/operations/document-citations.ts:28">
P2: A turn-local ref can resolve to the wrong workspace item when the same short ref appears in more than one transcript turn, because the map overwrites earlier records without checking their locations. Preserve only unambiguous refs (matching the collision handling used by `workspace-citations`) before writing the citation.</violation>
</file>

<file name="src/features/workspaces/documents/use-document-edit-review-overlay.ts">

<violation number="1" location="src/features/workspaces/documents/use-document-edit-review-overlay.ts:46">
P1: When a review fails to load or the server reports the receipt as unavailable (not_found, content_changed, not_latest, reverted, review_unavailable), the document editor is left permanently frozen in read-only mode. The effect calls `editor.setEditable(false)` up front, but the error and 'not ready' branches call `hideReview()` and `return` before any cleanup is registered; since `hideReview()` nulls the target, the effect re-runs and exits through the `if (!editor || !target) return;` guard without ever restoring `editor.setEditable(canEdit)`. The user is then stuck unable to edit that document until it is remounted. Consider restoring editability in those early-return branches (or always registering a cleanup that restores it) before calling hideReview().</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

onWorkspaceReferences: (records) => {
this._recordWorkspaceReferences(records);
},
resolveWorkspaceReferences: (refs) => this._resolveWorkspaceReferences(refs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Document citations are lost when the assistant edits through orchestrate: the resolver is wired into direct workspace tools here, but not into the separately constructed Code Mode workspace tools. Thread the resolver through createAIThreadTurnToolConfig and its tool catalog as well so both execution paths resolve <citation ref> locations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/ai-thread.ts, line 175:

<comment>Document citations are lost when the assistant edits through `orchestrate`: the resolver is wired into direct workspace tools here, but not into the separately constructed Code Mode workspace tools. Thread the resolver through `createAIThreadTurnToolConfig` and its tool catalog as well so both execution paths resolve `<citation ref>` locations.</comment>

<file context>
@@ -172,6 +172,7 @@ export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
 				onWorkspaceReferences: (records) => {
 					this._recordWorkspaceReferences(records);
 				},
+				resolveWorkspaceReferences: (refs) => this._resolveWorkspaceReferences(refs),
 			});
 		}
</file context>

// Freeze on request rather than on arrival: the verdict takes a round trip,
// and a document that accepts typing in the meantime is one whose marks
// will not match it.
editor.setEditable(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a review fails to load or the server reports the receipt as unavailable (not_found, content_changed, not_latest, reverted, review_unavailable), the document editor is left permanently frozen in read-only mode. The effect calls editor.setEditable(false) up front, but the error and 'not ready' branches call hideReview() and return before any cleanup is registered; since hideReview() nulls the target, the effect re-runs and exits through the if (!editor || !target) return; guard without ever restoring editor.setEditable(canEdit). The user is then stuck unable to edit that document until it is remounted. Consider restoring editability in those early-return branches (or always registering a cleanup that restores it) before calling hideReview().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/use-document-edit-review-overlay.ts, line 46:

<comment>When a review fails to load or the server reports the receipt as unavailable (not_found, content_changed, not_latest, reverted, review_unavailable), the document editor is left permanently frozen in read-only mode. The effect calls `editor.setEditable(false)` up front, but the error and 'not ready' branches call `hideReview()` and `return` before any cleanup is registered; since `hideReview()` nulls the target, the effect re-runs and exits through the `if (!editor || !target) return;` guard without ever restoring `editor.setEditable(canEdit)`. The user is then stuck unable to edit that document until it is remounted. Consider restoring editability in those early-return branches (or always registering a cleanup that restores it) before calling hideReview().</comment>

<file context>
@@ -0,0 +1,84 @@
+		// Freeze on request rather than on arrival: the verdict takes a round trip,
+		// and a document that accepts typing in the meantime is one whose marks
+		// will not match it.
+		editor.setEditable(false);
+
+		if (reviewQuery.isError) {
</file context>


if (editor.isActive("heading", { level: 1 })) {
return { kind: "fontSize", size: "32" };
for (const level of [1, 2, 3, 4] as const) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

This behavior change adds heading level 4 detection and renames the state union from DocumentFontSize to DocumentTextStyle, but no tests in the suite exercise getActiveBlock or the new textStyle values. A practical regression test (for example, mocking a TipTap editor and asserting the returned block for each heading level and the paragraph default) would prevent silent breakage of the toolbar state. Please add unit tests that cover the new heading4 branch and the semantic rename.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/document-editor/document-editor-state.ts, line 169:

<comment>This behavior change adds heading level 4 detection and renames the state union from `DocumentFontSize` to `DocumentTextStyle`, but no tests in the suite exercise `getActiveBlock` or the new `textStyle` values. A practical regression test (for example, mocking a TipTap editor and asserting the returned block for each heading level and the paragraph default) would prevent silent breakage of the toolbar state. Please add unit tests that cover the new `heading4` branch and the semantic rename.</comment>

<file context>
@@ -165,19 +166,13 @@ function getActiveBlock(editor: Editor): DocumentEditorUiState["block"] {
 
-	if (editor.isActive("heading", { level: 1 })) {
-		return { kind: "fontSize", size: "32" };
+	for (const level of [1, 2, 3, 4] as const) {
+		if (editor.isActive("heading", { level })) {
+			return { kind: "textStyle", style: `heading${level}` } as const;
</file context>

@@ -11,6 +11,7 @@ import {
Heading1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The toolbar semantics changed from font-size-based actions to text-style-based actions (paragraph and heading levels 1–4), including a new Heading 4 action with its own active state, run command, and icon mapping. However, there is no visible test coverage exercising this new behavior. Consider adding unit tests that assert the isTextStyle active-state logic, the getTextStyleIcon mappings, and the TipTap commands invoked by each documentTextStyleActions entry—including the new heading level 4—to prevent regressions in the document editor toolbar.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/document-editor/document-editor-toolbar-actions.tsx, line 80:

<comment>The toolbar semantics changed from font-size-based actions to text-style-based actions (paragraph and heading levels 1–4), including a new Heading 4 action with its own `active` state, `run` command, and icon mapping. However, there is no visible test coverage exercising this new behavior. Consider adding unit tests that assert the `isTextStyle` active-state logic, the `getTextStyleIcon` mappings, and the TipTap commands invoked by each `documentTextStyleActions` entry—including the new heading level 4—to prevent regressions in the document editor toolbar.</comment>

<file context>
@@ -42,39 +43,47 @@ export interface DocumentToolbarAction {
 		run: (editor) => editor.chain().focus().setHeading({ level: 3 }).run(),
 	},
+	{
+		id: "text-style-heading4",
+		icon: <Heading4 />,
+		label: "Heading 4",
</file context>

.then(() => session.persistence.get(localDocumentReadyKey))
.then((wasServerSynced) => {
if (wasServerSynced === localDocumentReadyValue) {
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The modified persistence-based readiness condition introduces a real behavior change, but there's no visible test coverage for it. A session that has the cached 'server-synced' marker in IndexedDB but an empty tiptap XML fragment will no longer be marked ready early and will instead wait for the websocket sync event. This edge case is now silently altered without regression protection. Consider adding a test that mocks the IndexedDB marker and toggles an empty vs. non-empty Y.XmlFragment to assert whether markReady() fires without a websocket sync — this directly exercises the change and guards against regressions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/use-document-collaboration-session.ts, line 207:

<comment>The modified persistence-based readiness condition introduces a real behavior change, but there's no visible test coverage for it. A session that has the cached 'server-synced' marker in IndexedDB but an empty tiptap XML fragment will no longer be marked ready early and will instead wait for the websocket sync event. This edge case is now silently altered without regression protection. Consider adding a test that mocks the IndexedDB marker and toggles an empty vs. non-empty `Y.XmlFragment` to assert whether `markReady()` fires without a websocket sync — this directly exercises the change and guards against regressions.</comment>

<file context>
@@ -203,7 +204,10 @@ function createActiveDocumentSession(input: {
 		.then(() => session.persistence.get(localDocumentReadyKey))
 		.then((wasServerSynced) => {
-			if (wasServerSynced === localDocumentReadyValue) {
+			if (
+				wasServerSynced === localDocumentReadyValue &&
+				session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0
</file context>


return applyDocumentCitationLocations(
input.html,
new Map(records.map((record) => [record.ref, record.location])),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A turn-local ref can resolve to the wrong workspace item when the same short ref appears in more than one transcript turn, because the map overwrites earlier records without checking their locations. Preserve only unambiguous refs (matching the collision handling used by workspace-citations) before writing the citation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/operations/document-citations.ts, line 28:

<comment>A turn-local ref can resolve to the wrong workspace item when the same short ref appears in more than one transcript turn, because the map overwrites earlier records without checking their locations. Preserve only unambiguous refs (matching the collision handling used by `workspace-citations`) before writing the citation.</comment>

<file context>
@@ -0,0 +1,30 @@
+
+	return applyDocumentCitationLocations(
+		input.html,
+		new Map(records.map((record) => [record.ref, record.location])),
+	);
+}
</file context>

prosemirrorJSONToYXmlFragment(getTiptapDocumentSchema(), document, fragment);
}, this);
private reconcileCurrentDocument(document: TiptapDocumentJson) {
const fragment = this.document.getXmlFragment(tiptapDocumentYjsField);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: reconcileCurrentDocument now repopulates an existing Yjs XmlFragment without clearing it first, whereas the previous replaceCurrentDocument explicitly did fragment.delete(0, fragment.length) and wrapped the write in this.document.transact(...). If prosemirrorJSONToYXmlFragment appends (as the old code's explicit delete strongly suggests), every AI edit and every ref reconciliation would duplicate the document's top-level blocks, and dropping the transact wrapper also removes the atomicity the old code guaranteed when streaming updates to connected clients. Please verify the library clears the fragment and restore the explicit clear/transaction (as in the removed code) if it does not.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-session.ts, line 443:

<comment>`reconcileCurrentDocument` now repopulates an existing Yjs `XmlFragment` without clearing it first, whereas the previous `replaceCurrentDocument` explicitly did `fragment.delete(0, fragment.length)` and wrapped the write in `this.document.transact(...)`. If `prosemirrorJSONToYXmlFragment` appends (as the old code's explicit delete strongly suggests), every AI edit and every ref reconciliation would duplicate the document's top-level blocks, and dropping the `transact` wrapper also removes the atomicity the old code guaranteed when streaming updates to connected clients. Please verify the library clears the fragment and restore the explicit clear/transaction (as in the removed code) if it does not.</comment>

<file context>
@@ -230,32 +354,94 @@ export class DocumentSession extends YServer {
-			prosemirrorJSONToYXmlFragment(getTiptapDocumentSchema(), document, fragment);
-		}, this);
+	private reconcileCurrentDocument(document: TiptapDocumentJson) {
+		const fragment = this.document.getXmlFragment(tiptapDocumentYjsField);
+		prosemirrorJSONToYXmlFragment(getTiptapDocumentSchema(), document, fragment);
 	}
</file context>

expect(html).toContain("<strong>bold</strong>");
expect(html).toContain('data-type="inline-math"');
expect(html).toContain('data-type="taskItem"');
expect(parseDocumentAiHtml(html)).toMatchObject({ type: "doc" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The round-trip test never verifies the round-trip: it only checks substrings and that re-parsing yields type: "doc", so silently dropped or reordered blocks still pass despite the test name and the PR's "remains lossless" claim. Consider asserting the re-parsed document deep-equals the original (or a normalized snapshot) to actually guard the round-trip.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-ai-html.test.ts, line 23:

<comment>The round-trip test never verifies the round-trip: it only checks substrings and that re-parsing yields `type: "doc"`, so silently dropped or reordered blocks still pass despite the test name and the PR's "remains lossless" claim. Consider asserting the re-parsed document deep-equals the original (or a normalized snapshot) to actually guard the round-trip.</comment>

<file context>
@@ -0,0 +1,71 @@
+		expect(html).toContain("<strong>bold</strong>");
+		expect(html).toContain('data-type="inline-math"');
+		expect(html).toContain('data-type="taskItem"');
+		expect(parseDocumentAiHtml(html)).toMatchObject({ type: "doc" });
+	});
+
</file context>
Suggested change
expect(parseDocumentAiHtml(html)).toMatchObject({ type: "doc" });
expect(parseDocumentAiHtml(html)).toEqual(document);

}

const page = Number(citation?.getAttribute("data-page"));
event.preventDefault();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Clicking a citation that points at an item that has since been deleted or renamed silently does nothing. The new click handler always calls event.preventDefault() and returns true as if the citation was actioned, but it discards the boolean reveal() returns (workspace-location-context.tsx returns Boolean(viewInstanceId), which is false when the target item no longer exists). Because the new styles.css gives citations cursor:pointer, a stale citation still looks clickable but produces no navigation and no feedback. Consider checking reveal()'s result (or hasItem) and, when it returns false, falling back to opening the plain document item or otherwise signalling that the source is unavailable, rather than swallowing the click.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx, line 103:

<comment>Clicking a citation that points at an item that has since been deleted or renamed silently does nothing. The new click handler always calls event.preventDefault() and returns true as if the citation was actioned, but it discards the boolean reveal() returns (workspace-location-context.tsx returns Boolean(viewInstanceId), which is false when the target item no longer exists). Because the new styles.css gives citations cursor:pointer, a stale citation still looks clickable but produces no navigation and no feedback. Consider checking reveal()'s result (or hasItem) and, when it returns false, falling back to opening the plain document item or otherwise signalling that the source is unavailable, rather than swallowing the click.</comment>

<file context>
@@ -85,6 +89,26 @@ function DocumentEditorInstance({
+					}
+
+					const page = Number(citation?.getAttribute("data-page"));
+					event.preventDefault();
+					reveal(
+						Number.isInteger(page) && page > 0
</file context>

return output;
}

return { ...parsed.data, calls: withoutCallActions(parsed.data.calls) };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The model-facing boundary strips app-only action from calls but does not strip the __thinkexUi/receipt metadata from result — it only stays clean because execute happens to feed it already-normalized (pre-stripped) output. If this exported helper is ever called with the raw runtime output (its signature accepts any output), the internal receipt ID would surface to the model. Applying stripAIThreadToolUiMetadata to result here (as normalizeAIThreadOrchestrationOutput already does) would make the boundary self-contained and consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/ai-thread-orchestration-contract.ts, line 208:

<comment>The model-facing boundary strips app-only `action` from calls but does not strip the `__thinkexUi`/receipt metadata from `result` — it only stays clean because `execute` happens to feed it already-normalized (pre-stripped) output. If this exported helper is ever called with the raw runtime output (its signature accepts any `output`), the internal receipt ID would surface to the model. Applying `stripAIThreadToolUiMetadata` to `result` here (as `normalizeAIThreadOrchestrationOutput` already does) would make the boundary self-contained and consistent.</comment>

<file context>
@@ -180,11 +194,25 @@ export function getAIThreadOrchestrationTelemetryOutput(output: unknown) {
+		return output;
+	}
+
+	return { ...parsed.data, calls: withoutCallActions(parsed.data.calls) };
+}
+
</file context>

Opening a review is one request, triggered by a click, used once. It was a
cached query with every cache feature switched off: a key made unique per open,
a zero collection time, a manual enabled flag, and a placeholder key for when
there was nothing to fetch. That is a caching layer configured never to cache.

Ask for the verdict in showReview and keep the document it was computed against
alongside the review itself. Staleness stops being a question rather than being
answered again: there is no second copy to go out of date.

Deletes the query module. The overlay is now a hook that shows marks when a
review names its document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/styles.css (1)

668-684: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Provide a keyboard equivalent for the citation interaction.

cursor: pointer and :hover indicate an interactive citation, but these rules provide no :focus-visible state. If clicking a citation opens a source or details, render it as a keyboard-focusable link or button and handle keyboard activation. If citations are not interactive, remove the pointer cursor and hover rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles.css` around lines 668 - 684, Update the citation interaction
represented by the .workspace-document-prose citation styles to use a
keyboard-focusable link or button with keyboard activation, and add a clear
:focus-visible state matching the hover treatment. If citations are not actually
interactive, remove cursor: pointer and the citation:hover rule instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/workspaces/ai/ai-thread-runtime.ts`:
- Line 164: Update src/features/workspaces/ai/ai-thread-runtime.ts at line 164
so createAIThreadTurnToolConfig accepts and forwards resolveWorkspaceReferences
to createAIThreadToolCatalog; update src/features/workspaces/ai/ai-thread.ts at
line 175 so _createTurnToolConfig passes this._resolveWorkspaceReferences into
createAIThreadTurnToolConfig.

In `@src/features/workspaces/documents/document-ai-html.ts`:
- Around line 267-270: Update the element metadata handling in
parseDocumentAiHtml so non-"pdf-page" locations remove any existing data-page
attribute after setting data-item-id. Preserve setting data-page from
location.pageNumber for PDF-page citations, ensuring item-level citations do not
retain stale page values.

In `@src/features/workspaces/documents/document-edit-review-context.tsx`:
- Around line 44-69: Update the showReview callback to track a monotonically
increasing request identifier for each invocation, and capture the current
identifier before awaiting getDocumentEditReceiptReviewFn. After the request
resolves, ignore the result—including errors and status handling—unless its
identifier is still the latest, so an older request cannot call setActiveReview
or display stale feedback over a newer selection.

In `@src/styles.css`:
- Around line 668-684: Update the `.workspace-document-prose citation` and
corresponding `citation:hover` selectors to target the class or data attribute
emitted by the citation renderer instead of the unknown `citation` type
selector. Ensure both rules preserve their existing styles and satisfy Stylelint
without broad exceptions.

---

Nitpick comments:
In `@src/styles.css`:
- Around line 668-684: Update the citation interaction represented by the
.workspace-document-prose citation styles to use a keyboard-focusable link or
button with keyboard activation, and add a clear :focus-visible state matching
the hover treatment. If citations are not actually interactive, remove cursor:
pointer and the citation:hover rule instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc351221-ba83-4b84-a334-c73a05d84079

📥 Commits

Reviewing files that changed from the base of the PR and between cc160e9 and 0b0820a.

📒 Files selected for processing (20)
  • src/features/workspaces/ai/ai-thread-runtime.ts
  • src/features/workspaces/ai/ai-thread.ts
  • src/features/workspaces/ai/workspace-tool-result-adapters.ts
  • src/features/workspaces/ai/workspace-tools.ts
  • src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx
  • src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx
  • src/features/workspaces/documents/document-ai-edits.ts
  • src/features/workspaces/documents/document-ai-html.ts
  • src/features/workspaces/documents/document-edit-receipt.ts
  • src/features/workspaces/documents/document-edit-review-context.tsx
  • src/features/workspaces/documents/document-session.ts
  • src/features/workspaces/documents/tiptap-schema.ts
  • src/features/workspaces/documents/use-document-edit-receipt-undo.ts
  • src/features/workspaces/documents/use-document-edit-review-overlay.ts
  • src/features/workspaces/operations/create-items.ts
  • src/features/workspaces/operations/document-citations.ts
  • src/features/workspaces/operations/edit-item.ts
  • src/features/workspaces/operations/workspace-access-context.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
  • src/styles.css
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/features/workspaces/documents/use-document-edit-receipt-undo.ts
  • src/features/workspaces/operations/edit-item.ts
  • src/features/workspaces/ai/workspace-tool-result-adapters.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
  • src/features/workspaces/documents/document-ai-edits.ts
  • src/features/workspaces/documents/document-session.ts

const workspaceTools = createAIThreadWorkspaceTools({
getThreadContext: input.getThreadContext,
onWorkspaceReferences: input.onWorkspaceReferences,
resolveWorkspaceReferences: input.resolveWorkspaceReferences,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pass resolveWorkspaceReferences through the orchestration path.

The direct tool catalog receives the resolver, but the turn tool catalog does not. As a result, codemode document operations cannot resolve wr_ citations and unresolved citations become plain text during document parsing.

  • src/features/workspaces/ai/ai-thread-runtime.ts#L164-L164: Add resolveWorkspaceReferences to createAIThreadTurnToolConfig and forward it to createAIThreadToolCatalog.
  • src/features/workspaces/ai/ai-thread.ts#L175-L175: Pass this._resolveWorkspaceReferences from _createTurnToolConfig to createAIThreadTurnToolConfig.
📍 Affects 2 files
  • src/features/workspaces/ai/ai-thread-runtime.ts#L164-L164 (this comment)
  • src/features/workspaces/ai/ai-thread.ts#L175-L175
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/ai/ai-thread-runtime.ts` at line 164, Update
src/features/workspaces/ai/ai-thread-runtime.ts at line 164 so
createAIThreadTurnToolConfig accepts and forwards resolveWorkspaceReferences to
createAIThreadToolCatalog; update src/features/workspaces/ai/ai-thread.ts at
line 175 so _createTurnToolConfig passes this._resolveWorkspaceReferences into
createAIThreadTurnToolConfig.

Comment on lines +267 to +270
element.setAttribute("data-item-id", location.itemId);
if (location.kind === "pdf-page") {
element.setAttribute("data-page", String(location.pageNumber));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Clear stale data-page metadata for non-PDF locations.

If the input citation already has data-page and location.kind is not "pdf-page", this code retains the old page value. parseDocumentAiHtml then stores a page number for an item-level citation.

Proposed fix
 		element.setAttribute("data-item-id", location.itemId);
 		if (location.kind === "pdf-page") {
 			element.setAttribute("data-page", String(location.pageNumber));
+		} else {
+			element.removeAttribute("data-page");
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
element.setAttribute("data-item-id", location.itemId);
if (location.kind === "pdf-page") {
element.setAttribute("data-page", String(location.pageNumber));
}
element.setAttribute("data-item-id", location.itemId);
if (location.kind === "pdf-page") {
element.setAttribute("data-page", String(location.pageNumber));
} else {
element.removeAttribute("data-page");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/documents/document-ai-html.ts` around lines 267 -
270, Update the element metadata handling in parseDocumentAiHtml so
non-"pdf-page" locations remove any existing data-page attribute after setting
data-item-id. Preserve setting data-page from location.pageNumber for PDF-page
citations, ensuring item-level citations do not retain stale page values.

Comment on lines +44 to +69
async (input: { itemId: string; receiptIds: string[] }) => {
// reveal opens the document, or focuses the tab already holding it, and
// only fails when the item is gone.
if (!reveal({ itemId: input.itemId, kind: "item", version: 1 })) {
toast.error("This document no longer exists.");
return;
}

const review = await getDocumentEditReceiptReviewFn({
data: { itemId: input.itemId, receiptIds: input.receiptIds, workspaceId },
}).catch(() => null);

if (!review) {
toast.error("Could not load these changes.");
return;
}
if (review.status !== "ready") {
toast.error(unavailableReviewMessages[review.status]);
return;
}

setActiveReview({
beforeDocument: review.beforeDocument,
itemId: input.itemId,
receiptIds: input.receiptIds,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent an older review request from replacing a newer selection.

showReview calls can overlap. If request A starts, then request B starts, and A resolves last, Line 65 replaces B's active review with A's result. The open document can then have no overlay while another document receives the stale review state.

Track the latest request and ignore results from earlier requests.

Proposed fix
+const latestReviewRequest = useRef(0);
+
 const showReview = useCallback(
   async (input: { itemId: string; receiptIds: string[] }) => {
+    const requestId = ++latestReviewRequest.current;
     // reveal opens the document...
     if (!reveal({ itemId: input.itemId, kind: "item", version: 1 })) {
       toast.error("This document no longer exists.");
       return;
     }

     const review = await getDocumentEditReceiptReviewFn({
       data: { itemId: input.itemId, receiptIds: input.receiptIds, workspaceId },
     }).catch(() => null);

+    if (requestId !== latestReviewRequest.current) {
+      return;
+    }
+
     // Existing result handling.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async (input: { itemId: string; receiptIds: string[] }) => {
// reveal opens the document, or focuses the tab already holding it, and
// only fails when the item is gone.
if (!reveal({ itemId: input.itemId, kind: "item", version: 1 })) {
toast.error("This document no longer exists.");
return;
}
const review = await getDocumentEditReceiptReviewFn({
data: { itemId: input.itemId, receiptIds: input.receiptIds, workspaceId },
}).catch(() => null);
if (!review) {
toast.error("Could not load these changes.");
return;
}
if (review.status !== "ready") {
toast.error(unavailableReviewMessages[review.status]);
return;
}
setActiveReview({
beforeDocument: review.beforeDocument,
itemId: input.itemId,
receiptIds: input.receiptIds,
});
async (input: { itemId: string; receiptIds: string[] }) => {
const requestId = ++latestReviewRequest.current;
// reveal opens the document, or focuses the tab already holding it, and
// only fails when the item is gone.
if (!reveal({ itemId: input.itemId, kind: "item", version: 1 })) {
toast.error("This document no longer exists.");
return;
}
const review = await getDocumentEditReceiptReviewFn({
data: { itemId: input.itemId, receiptIds: input.receiptIds, workspaceId },
}).catch(() => null);
if (requestId !== latestReviewRequest.current) {
return;
}
if (!review) {
toast.error("Could not load these changes.");
return;
}
if (review.status !== "ready") {
toast.error(unavailableReviewMessages[review.status]);
return;
}
setActiveReview({
beforeDocument: review.beforeDocument,
itemId: input.itemId,
receiptIds: input.receiptIds,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/documents/document-edit-review-context.tsx` around
lines 44 - 69, Update the showReview callback to track a monotonically
increasing request identifier for each invocation, and capture the current
identifier before awaiting getDocumentEditReceiptReviewFn. After the request
resolves, ignore the result—including errors and status handling—unless its
identifier is still the latest, so an older request cannot call setActiveReview
or display stale feedback over a newer selection.

Comment thread src/styles.css Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 issues found across 58 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/documents/document-ai-edits.ts">

<violation number="1" location="src/features/workspaces/documents/document-ai-edits.ts:181">
P2: The receipt's line tally counts atom blocks (horizontal rule, math) by stringifying `node.toJSON()`, which includes the top-level `aiRef` attribute on every block. Because the `aiRef` fingerprint is part of the serialized line, an unchanged formula/hr block whose ref was regenerated by a `replace_all` edit will be counted as both a line removed and a line added, inflating the receipt's "Lines: +N −M" totals even though the block's content never changed. This is inconsistent with the text-block branch, which strips refs by using only `node.textContent`. Consider normalizing refs away before stringifying the atom, matching the `withoutTopLevelAiRefs`/`withTiptapNodeAiRef(node, null)` approach used elsewhere.</violation>
</file>

<file name="src/features/workspaces/documents/document-ai-html.ts">

<violation number="1" location="src/features/workspaces/documents/document-ai-html.ts:67">
P3: An unresolved citation (no data-item-id) that sits at the document root gets replaced by a plain-text node before validation, and then validateDocumentAiHtml rejects any non-whitespace top-level text node as "Plain text and Markdown are not accepted." So at the root level the documented design ("becomes its own label rather than failing the write") doesn't hold — the edit still fails, and with a misleading non-HTML message. If a resolver-less citation should degrade to its label, consider wrapping it in a paragraph (or skipping the top-level text check for known-good replacements) so the write doesn't fail here.</violation>

<violation number="2" location="src/features/workspaces/documents/document-ai-html.ts:268">
P2: When a citation already has `data-page` and the resolved location is not `pdf-page`, this branch leaves the old page attribute in place. That stale metadata can make an item-level citation look page-specific on later parse/read cycles. Clearing `data-page` in the non-PDF case would prevent that carry-over.</violation>
</file>

<file name="src/features/workspaces/operations/create-items.ts">

<violation number="1" location="src/features/workspaces/operations/create-items.ts:121">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The `createWorkspaceItemsOperation` function now resolves citations via `resolveDocumentCitations` and parses `initialContent` as HTML through `parseDocumentAiHtml` (replacing the previous Markdown path). This is a meaningful behavior change: non-HTML content that was previously tolerated is now rejected as `invalid_initial_content`, and the `warnings` field has been removed entirely. However, there are no tests exercising this changed behavior in `create-items.ts`. Practical regression assertions — e.g., validating that HTML content succeeds, non-HTML returns `invalid_initial_content`, and citations are resolved during creation — are missing. Adding tests for the create-items path would ensure the new parsing and citation behavior is properly validated.</violation>

<violation number="2" location="src/features/workspaces/operations/create-items.ts:303">
P2: Created documents can contain fabricated citation targets: an AI-supplied `<citation data-item-id="...">` bypasses `resolveDocumentCitations` and is persisted as a clickable citation without a verified workspace reference. Normalize or reject `data-item-id` in model HTML and only retain IDs produced by resolving a valid `ref`.</violation>
</file>

<file name="src/features/workspaces/documents/use-document-collaboration-session.ts">

<violation number="1" location="src/features/workspaces/documents/use-document-collaboration-session.ts:209">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

This PR changes the document-session readiness logic by adding an extra `session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0` check to the IndexedDB cache path. Previously, a cached `server-synced` flag alone would mark the session ready; now an empty Yjs fragment prevents that. If a user is offline with an empty or newly-created previously-synced document, the session will never become ready because the provider `sync` handler won't fire and the IndexedDB cache path is now gated. This is a real behavior change with a clear offline regression, yet no tests cover this file or the new condition. Please add a regression test asserting that a non-empty fragment with a cached sync flag marks ready, while an empty fragment with the same flag defers readiness.</violation>
</file>

<file name="src/features/workspaces/components/document-editor/document-editor-toolbar-actions.tsx">

<violation number="1" location="src/features/workspaces/components/document-editor/document-editor-toolbar-actions.tsx:79">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `heading4` toolbar action and the broader refactor from `fontSize` to `textStyle` block kinds are user-visible behavior changes, yet no tests cover these additions. The `document-editor` component directory has no test files at all, and no regression-style assertions verify that `documentTextStyleActions` includes `heading4`, that `getTextStyleIcon` returns the correct icon, or that `isTextStyle` matches the new `textStyle` kind. Practical unit tests for these exports would be straightforward and should be added to guard against future regressions.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts:7">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `ai-thread-tool-ui-metadata.ts` module introduces runtime behavior for attaching, reading, and stripping AI tool output metadata, but has no test coverage. Regression-style assertions are very practical for these pure utilities (attach→get round-trip, recursive strip at various nesting depths, pass-through for non-objects). Please add unit tests for `attachDocumentEditReceiptMetadata`, `getDocumentEditReceiptMetadata`, and `stripAIThreadToolUiMetadata`.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread-orchestration.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread-orchestration.ts:80">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

This PR adds a new `toModelOutput` property to the orchestration tool that wraps `getAIThreadOrchestrationModelOutput`, but no test verifies that the returned tool actually includes this property or that it returns the expected `{ type: "json", value: ... }` shape. While the underlying helper is tested separately, the integration point is not. A regression assertion here is practical and would prevent silent breakage of the model-output projection.</violation>

<violation number="2" location="src/features/workspaces/ai/ai-thread-orchestration.ts:133">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The connector now conditionally attaches document-edit receipt metadata for `workspace_edit_item`, but this execution path is not exercised by any visible test. The `ai-thread-orchestration.worker.test.ts` mocks `createExecuteRuntime` entirely and only tests a generic "nested" tool through `connector.tools()`, not `workspace_edit_item`. `attachDocumentEditReceiptMetadata` has zero references in test files. A practical regression test would exercise `workspace_edit_item` through the connector and assert receipt metadata is present, while verifying other tools do not receive it.</violation>
</file>

<file name="src/features/workspaces/documents/document-edit-review-extension.ts">

<violation number="1" location="src/features/workspaces/documents/document-edit-review-extension.ts:74">
P3: While a review is open, every document transaction (including each remote collaborative edit) replays a full-document diff: createDocumentEditReviewDecorations re-parses the entire before-document JSON into a node and runs a whole-document ChangeSet comparison against the current content. For long documents with frequent collaborators this is repeated O(doc-size) work that could be done incrementally or memoized (cache the parsed before-node/schema outside the per-transaction path). Low risk for small docs, but worth guarding given the feature is explicitly scoped around larger documents elsewhere (review_unavailable).</violation>

<violation number="2" location="src/features/workspaces/documents/document-edit-review-extension.ts:167">
P2: Changes to atom blocks nested inside lists or blockquotes can appear with no review decoration, leaving users unable to see those AI edits. Traversing descendant nodes when finding changed atoms would preserve the intended block marker for nested content.</violation>
</file>

<file name="src/features/workspaces/content/workspace-content-reader.ts">

<violation number="1" location="src/features/workspaces/content/workspace-content-reader.ts:98">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The `readBudgetExhausted` flag changes the semantics of batch reading: after the first item exceeds the remaining byte budget, every later item is rejected without being read, even if it would fit. The PR does not document this behavior change, and the existing budget test (`bounds total content returned by a batch`) creates identical oversized items so it passes under both the old and new behavior. There is no test asserting the new hard-stop semantics, which means a small item after an oversized one is silently dropped. Consider either restoring the per-item budget check so smaller trailing items are still evaluated, or adding an explicit test that documents and asserts the new stop-after-first-rejection behavior.</violation>

<violation number="2" location="src/features/workspaces/content/workspace-content-reader.ts:107">
P3: The relations RPCs are now started without being awaited and only consumed in attachRelationPaths. In the normal path this is a fine parallelization. However, if a later read in the loop throws a non-WorkspacePageSelectionError (which is rethrown and aborts before attachRelationPaths runs), the in-flight listItemRelations promises for earlier ready items are left un-awaited and could surface as unhandled promise rejections if they reject. Consider collecting the promises and ensuring they are always awaited or attached a rejection handler so no kernel rejection is left dangling.</violation>
</file>

<file name="src/features/workspaces/documents/document-edit-review-context.tsx">

<violation number="1" location="src/features/workspaces/documents/document-edit-review-context.tsx:52">
P3: showReview races when the user opens reviews for two documents in quick succession. The RPC for the first document can resolve after the second one's, and setActiveReview simply overwrites, so the visible review and the on-screen document can end up out of sync (the overlay only applies when activeReview.itemId matches the mounted document). Consider guarding the async application—e.g., keep a ref of the most recently requested itemId and ignore any response that isn't for it, or abort/cancel the prior request before issuing a new one.</violation>
</file>

<file name="src/features/workspaces/documents/document-session.ts">

<violation number="1" location="src/features/workspaces/documents/document-session.ts:442">
P1: The new `reconcileCurrentDocument` replaces the old `replaceCurrentDocument`, but it drops the explicit `fragment.delete(0, fragment.length)` (and the `transact` wrapper) that the previous implementation performed before calling `prosemirrorJSONToYXmlFragment`. Since every caller of this helper (applyEdits, undoDocumentEditReceipt, getReferencedDocumentSnapshot) writes into a fragment that already holds content, correct replacement depends on `prosemirrorJSONToYXmlFragment` clearing the target fragment itself — which is not guaranteed by the library contract. If it merely appends/inserts, each AI edit or undo would silently duplicate the document content. Please confirm the library behavior and, if it does not clear, restore an explicit piecewise clear (ideally inside a transaction, as before) before merging.</violation>
</file>

<file name="src/features/workspaces/documents/document-edit-review-functions.ts">

<violation number="1" location="src/features/workspaces/documents/document-edit-review-functions.ts:23">
P2: This review RPC is a GET but can carry a large payload. The schema allows up to 40 receipt ids, each up to 512 characters, and TanStack Start encodes the validator data into the URL for GET requests, so a receipt-heavy turn can push the query string toward a size that trips intermediary URL-length limits and fail to load the review silently. Using method: "POST" (undo already is) avoids the URL-size constraint, or the caps can be tightened.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

fragment.delete(0, fragment.length);
prosemirrorJSONToYXmlFragment(getTiptapDocumentSchema(), document, fragment);
}, this);
private reconcileCurrentDocument(document: TiptapDocumentJson) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new reconcileCurrentDocument replaces the old replaceCurrentDocument, but it drops the explicit fragment.delete(0, fragment.length) (and the transact wrapper) that the previous implementation performed before calling prosemirrorJSONToYXmlFragment. Since every caller of this helper (applyEdits, undoDocumentEditReceipt, getReferencedDocumentSnapshot) writes into a fragment that already holds content, correct replacement depends on prosemirrorJSONToYXmlFragment clearing the target fragment itself — which is not guaranteed by the library contract. If it merely appends/inserts, each AI edit or undo would silently duplicate the document content. Please confirm the library behavior and, if it does not clear, restore an explicit piecewise clear (ideally inside a transaction, as before) before merging.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-session.ts, line 442:

<comment>The new `reconcileCurrentDocument` replaces the old `replaceCurrentDocument`, but it drops the explicit `fragment.delete(0, fragment.length)` (and the `transact` wrapper) that the previous implementation performed before calling `prosemirrorJSONToYXmlFragment`. Since every caller of this helper (applyEdits, undoDocumentEditReceipt, getReferencedDocumentSnapshot) writes into a fragment that already holds content, correct replacement depends on `prosemirrorJSONToYXmlFragment` clearing the target fragment itself — which is not guaranteed by the library contract. If it merely appends/inserts, each AI edit or undo would silently duplicate the document content. Please confirm the library behavior and, if it does not clear, restore an explicit piecewise clear (ideally inside a transaction, as before) before merging.</comment>

<file context>
@@ -230,32 +354,94 @@ export class DocumentSession extends YServer {
-			fragment.delete(0, fragment.length);
-			prosemirrorJSONToYXmlFragment(getTiptapDocumentSchema(), document, fragment);
-		}, this);
+	private reconcileCurrentDocument(document: TiptapDocumentJson) {
+		const fragment = this.document.getXmlFragment(tiptapDocumentYjsField);
+		prosemirrorJSONToYXmlFragment(getTiptapDocumentSchema(), document, fragment);
</file context>

if (wasServerSynced === localDocumentReadyValue) {
if (
wasServerSynced === localDocumentReadyValue &&
session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag AI Slop and Fabricated Changes

This PR changes the document-session readiness logic by adding an extra session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0 check to the IndexedDB cache path. Previously, a cached server-synced flag alone would mark the session ready; now an empty Yjs fragment prevents that. If a user is offline with an empty or newly-created previously-synced document, the session will never become ready because the provider sync handler won't fire and the IndexedDB cache path is now gated. This is a real behavior change with a clear offline regression, yet no tests cover this file or the new condition. Please add a regression test asserting that a non-empty fragment with a cached sync flag marks ready, while an empty fragment with the same flag defers readiness.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/use-document-collaboration-session.ts, line 209:

<comment>This PR changes the document-session readiness logic by adding an extra `session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0` check to the IndexedDB cache path. Previously, a cached `server-synced` flag alone would mark the session ready; now an empty Yjs fragment prevents that. If a user is offline with an empty or newly-created previously-synced document, the session will never become ready because the provider `sync` handler won't fire and the IndexedDB cache path is now gated. This is a real behavior change with a clear offline regression, yet no tests cover this file or the new condition. Please add a regression test asserting that a non-empty fragment with a cached sync flag marks ready, while an empty fragment with the same flag defers readiness.</comment>

<file context>
@@ -203,7 +204,10 @@ function createActiveDocumentSession(input: {
-			if (wasServerSynced === localDocumentReadyValue) {
+			if (
+				wasServerSynced === localDocumentReadyValue &&
+				session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0
+			) {
 				markReady();
</file context>


return {
content: stringifyTiptapDocumentJson(projection.document),
content: stringifyTiptapDocumentJson(parseDocumentAiHtml(input.initialContent)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Created documents can contain fabricated citation targets: an AI-supplied <citation data-item-id="..."> bypasses resolveDocumentCitations and is persisted as a clickable citation without a verified workspace reference. Normalize or reject data-item-id in model HTML and only retain IDs produced by resolving a valid ref.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/operations/create-items.ts, line 303:

<comment>Created documents can contain fabricated citation targets: an AI-supplied `<citation data-item-id="...">` bypasses `resolveDocumentCitations` and is persisted as a clickable citation without a verified workspace reference. Normalize or reject `data-item-id` in model HTML and only retain IDs produced by resolving a valid `ref`.</comment>

<file context>
@@ -293,12 +299,9 @@ function getCreateWorkspaceItemInitialContent(input: CreateWorkspaceItemOperatio
-
 		return {
-			content: stringifyTiptapDocumentJson(projection.document),
+			content: stringifyTiptapDocumentJson(parseDocumentAiHtml(input.initialContent)),
 			status: "ready",
-			...(projection.warnings.length > 0 ? { warnings: projection.warnings } : {}),
</file context>

Comment thread src/features/workspaces/documents/use-document-edit-receipt-undo.ts Outdated
Comment thread src/features/workspaces/documents/use-document-edit-receipt-undo.ts Outdated
status: "failed",
...(resolution.item.type === "file" ? { type: "file" as const } : {}),
});
readBudgetExhausted = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The readBudgetExhausted flag changes the semantics of batch reading: after the first item exceeds the remaining byte budget, every later item is rejected without being read, even if it would fit. The PR does not document this behavior change, and the existing budget test (bounds total content returned by a batch) creates identical oversized items so it passes under both the old and new behavior. There is no test asserting the new hard-stop semantics, which means a small item after an oversized one is silently dropped. Consider either restoring the per-item budget check so smaller trailing items are still evaluated, or adding an explicit test that documents and asserts the new stop-after-first-rejection behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/content/workspace-content-reader.ts, line 98:

<comment>The `readBudgetExhausted` flag changes the semantics of batch reading: after the first item exceeds the remaining byte budget, every later item is rejected without being read, even if it would fit. The PR does not document this behavior change, and the existing budget test (`bounds total content returned by a batch`) creates identical oversized items so it passes under both the old and new behavior. There is no test asserting the new hard-stop semantics, which means a small item after an oversized one is silently dropped. Consider either restoring the per-item budget check so smaller trailing items are still evaluated, or adding an explicit test that documents and asserts the new stop-after-first-rejection behavior.</comment>

<file context>
@@ -86,20 +95,16 @@ export async function readWorkspaceContent(input: {
-					status: "failed",
-					...(resolution.item.type === "file" ? { type: "file" as const } : {}),
-				});
+				readBudgetExhausted = true;
+				results.push(readBudgetFailure);
 				continue;
</file context>

return;
}

const review = await getDocumentEditReceiptReviewFn({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: showReview races when the user opens reviews for two documents in quick succession. The RPC for the first document can resolve after the second one's, and setActiveReview simply overwrites, so the visible review and the on-screen document can end up out of sync (the overlay only applies when activeReview.itemId matches the mounted document). Consider guarding the async application—e.g., keep a ref of the most recently requested itemId and ignore any response that isn't for it, or abort/cancel the prior request before issuing a new one.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-edit-review-context.tsx, line 52:

<comment>showReview races when the user opens reviews for two documents in quick succession. The RPC for the first document can resolve after the second one's, and setActiveReview simply overwrites, so the visible review and the on-screen document can end up out of sync (the overlay only applies when activeReview.itemId matches the mounted document). Consider guarding the async application—e.g., keep a ref of the most recently requested itemId and ignore any response that isn't for it, or abort/cancel the prior request before issuing a new one.</comment>

<file context>
@@ -0,0 +1,96 @@
+				return;
+			}
+
+			const review = await getDocumentEditReceiptReviewFn({
+				data: { itemId: input.itemId, receiptIds: input.receiptIds, workspaceId },
+			}).catch(() => null);
</file context>

item: resolution.item,
read,
relations: await input.kernel.listItemRelations({ itemId: resolution.item.id }),
relations: input.kernel.listItemRelations({ itemId: resolution.item.id }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The relations RPCs are now started without being awaited and only consumed in attachRelationPaths. In the normal path this is a fine parallelization. However, if a later read in the loop throws a non-WorkspacePageSelectionError (which is rethrown and aborts before attachRelationPaths runs), the in-flight listItemRelations promises for earlier ready items are left un-awaited and could surface as unhandled promise rejections if they reject. Consider collecting the promises and ensuring they are always awaited or attached a rejection handler so no kernel rejection is left dangling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/content/workspace-content-reader.ts, line 107:

<comment>The relations RPCs are now started without being awaited and only consumed in attachRelationPaths. In the normal path this is a fine parallelization. However, if a later read in the loop throws a non-WorkspacePageSelectionError (which is rethrown and aborts before attachRelationPaths runs), the in-flight listItemRelations promises for earlier ready items are left un-awaited and could surface as unhandled promise rejections if they reject. Consider collecting the promises and ensuring they are always awaited or attached a rejection handler so no kernel rejection is left dangling.</comment>

<file context>
@@ -86,20 +95,16 @@ export async function readWorkspaceContent(input: {
 				item: resolution.item,
 				read,
-				relations: await input.kernel.listItemRelations({ itemId: resolution.item.id }),
+				relations: input.kernel.listItemRelations({ itemId: resolution.item.id }),
 			};
 			readyResults.push(pending);
</file context>


// A citation the operation could not resolve to a real item cannot navigate
// anywhere, so it becomes its own label rather than failing the write.
for (const element of htmlDocument.body.querySelectorAll("citation:not([data-item-id])")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: An unresolved citation (no data-item-id) that sits at the document root gets replaced by a plain-text node before validation, and then validateDocumentAiHtml rejects any non-whitespace top-level text node as "Plain text and Markdown are not accepted." So at the root level the documented design ("becomes its own label rather than failing the write") doesn't hold — the edit still fails, and with a misleading non-HTML message. If a resolver-less citation should degrade to its label, consider wrapping it in a paragraph (or skipping the top-level text check for known-good replacements) so the write doesn't fail here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-ai-html.ts, line 67:

<comment>An unresolved citation (no data-item-id) that sits at the document root gets replaced by a plain-text node before validation, and then validateDocumentAiHtml rejects any non-whitespace top-level text node as "Plain text and Markdown are not accepted." So at the root level the documented design ("becomes its own label rather than failing the write") doesn't hold — the edit still fails, and with a misleading non-HTML message. If a resolver-less citation should degrade to its label, consider wrapping it in a paragraph (or skipping the top-level text check for known-good replacements) so the write doesn't fail here.</comment>

<file context>
@@ -0,0 +1,278 @@
+
+	// A citation the operation could not resolve to a real item cannot navigate
+	// anywhere, so it becomes its own label rather than failing the write.
+	for (const element of htmlDocument.body.querySelectorAll("citation:not([data-item-id])")) {
+		element.replaceWith(htmlDocument.createTextNode(element.textContent ?? ""));
+	}
</file context>

return review;
}

return {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: While a review is open, every document transaction (including each remote collaborative edit) replays a full-document diff: createDocumentEditReviewDecorations re-parses the entire before-document JSON into a node and runs a whole-document ChangeSet comparison against the current content. For long documents with frequent collaborators this is repeated O(doc-size) work that could be done incrementally or memoized (cache the parsed before-node/schema outside the per-transaction path). Low risk for small docs, but worth guarding given the feature is explicitly scoped around larger documents elsewhere (review_unavailable).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-edit-review-extension.ts, line 74:

<comment>While a review is open, every document transaction (including each remote collaborative edit) replays a full-document diff: createDocumentEditReviewDecorations re-parses the entire before-document JSON into a node and runs a whole-document ChangeSet comparison against the current content. For long documents with frequent collaborators this is repeated O(doc-size) work that could be done incrementally or memoized (cache the parsed before-node/schema outside the per-transaction path). Low risk for small docs, but worth guarding given the feature is explicitly scoped around larger documents elsewhere (review_unavailable).</comment>

<file context>
@@ -0,0 +1,204 @@
+							return review;
+						}
+
+						return {
+							beforeDocument,
+							decorations: createDocumentEditReviewDecorations(beforeDocument, newState.doc),
</file context>

urjitc and others added 4 commits August 1, 2026 13:21
The pill showed whatever the assistant wrote inside the tag, falling back to
"Source". Two of three models write nothing there, correctly: the citation
prompt tells them the element must be empty. So most citations would have read
"Source", while the same citation in chat reads the item's name.

Name it from the item when the ref is resolved, with the page when there is
one, and stop asking the assistant for a label it was told not to give.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A ref the assistant invented resolves to nothing, and asking the kernel for the
paths of zero items is a round trip for an empty answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Where a citation points and what it is called are stored separately, so a
rewrite that echoes the attributes and drops the text leaves a source that
navigates correctly and reads "Source". Models normalise markup; that would
have happened quietly, once per rewrite, and never repaired itself.

Name every citation with a resolvable item on every write, not only the ones
just cited.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stored label existed only to avoid a node view, and it brought its own
problems: it went stale when a source was renamed, it invented a custom-label
idea chat does not have, and a rewrite that dropped the text left a citation
called nothing - which needed a repair pass to undo.

Chat never stores a name. It resolves the location and reads the name from the
workspace as it stands. Do that: the editor swaps in a node view over the same
node spec, the same way it swaps in the highlighted code block, and renders the
very chip a chat reply renders.

A citation now stores what it points at and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/documents/document-edit-review-functions.ts">

<violation number="1" location="src/features/workspaces/documents/document-edit-review-functions.ts:40">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The PR introduces new server functions `getDocumentEditReceiptReviewFn` and `undoDocumentEditReceiptFn` that implement the core review/undo behavior for AI document edits, but no tests exercise these handlers. Adding regression-style tests for the validation schema, authorization branching (`read` vs `mutate`), and the delegation to the document session would be practical and would help prevent regressions in this new workflow.</violation>
</file>

<file name="src/features/workspaces/operations/create-items.ts">

<violation number="1" location="src/features/workspaces/operations/create-items.ts:125">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The behavioral change in how initial document content is processed — now flowing through `resolveDocumentCitations` and `parseDocumentAiHtml` instead of the old Markdown projection — is not exercised by any tests for this operation. I found no test files covering `create-items.ts`, `createWorkspaceItemsOperation`, `resolveDocumentCitations`, or `initialContent`. While the underlying parser has unit tests, the integration path in this file is entirely untested, and a regression-style assertion (e.g., creating an item with HTML content and asserting successful parsing and no warnings) is practical. Consider adding tests that exercise the new HTML/citation pipeline through `createWorkspaceItemsOperation` and assert the removal of `warnings` from the result.</violation>
</file>

<file name="src/features/workspaces/documents/document-edit-review-extension.ts">

<violation number="1" location="src/features/workspaces/documents/document-edit-review-extension.ts:76">
P3: While a review is active the document is read-only, but it is still collaborative — any remote teammate typing arrives as a `docChanged` transaction, and the plugin's `apply` recomputes a full-document `ChangeSet` diff (plus a per-change `document.forEach` in `addChangedAtomDecorations`) on every such transaction. For a large or actively-edited document this is a meaningful recompute repeated for the whole duration of the review. Consider caching/recomputing the decorations only when the review is shown (or at most debounced/throttled), since interacting with the review itself doesn't change the doc while it is read-only.</violation>

<violation number="2" location="src/features/workspaces/documents/document-edit-review-extension.ts:111">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new review-mark derivation logic (ChangeSet diffing, custom TokenEncoder, `simplifyChanges`, and decoration creation for insertions, deletions, and atom blocks) is not accompanied by tests in this diff. Behavior-change/feature PRs should exercise changed behavior with assertions rather than leaving complex logic unverified. Consider adding unit tests that exercise `createDocumentEditReviewDecorations` with concrete before/after documents and assert the resulting `DecorationSet` for insertions, deletions, attribute-only changes, and atom-block changes.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread.ts:521">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `_resolveWorkspaceReferences` method combines transcript references with in-flight `activeWorkspaceReferences` to let tools resolve refs from the current turn before they hit the transcript. This is a non-trivial behavior change in a behavior-change PR, yet no visible tests exercise this path. A regression-style test (e.g., simulating a document read followed by a ref citation within the same turn) would be practical and should be added to verify the merging logic works correctly.</violation>
</file>

<file name="src/features/workspaces/documents/use-document-collaboration-session.ts">

<violation number="1" location="src/features/workspaces/documents/use-document-collaboration-session.ts:209">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The added empty-fragment guard changes when a collaboration session is marked ready, introducing a new edge case where a previously-synced but empty document never reaches ready until a server round-trip. A regression-style test is practical here (e.g. asserting that `markReady` is withheld when the IndexedDB marker is present but the Yjs XML fragment is empty), yet no tests cover this file or the readiness logic. Consider adding a test for this guard so future changes to the restore path have coverage.</violation>
</file>

<file name="src/features/workspaces/documents/document-edit-review-context.tsx">

<violation number="1" location="src/features/workspaces/documents/document-edit-review-context.tsx:43">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The `showReview` callback and related review-state logic in this new context file are not covered by any unit test. Because this is a behavior-change PR introducing a review/undo workflow, Rule 1 expects new behavior to be exercised by tests when practical. Consider adding tests that cover the async `showReview` flow (successful load, each unavailable status mapping to the correct toast message, and state transitions) as well as `hideReview` and the missing-provider error path.</violation>

<violation number="2" location="src/features/workspaces/documents/document-edit-review-context.tsx:45">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The comment claims `reveal` "only fails when the item is gone," but `reveal` simply returns `Boolean(navigate(location))` where `navigate` is an external dependency. It could fail for layout or navigation reasons unrelated to item existence, and the context already exposes a separate `hasItem` for that purpose. The resulting toast message therefore may misattribute the failure. Consider removing the unfounded "only fails" claim so the comment describes behavior without asserting a contract that isn't guaranteed.</violation>

<violation number="3" location="src/features/workspaces/documents/document-edit-review-context.tsx:65">
P2: A slower earlier review request can replace the review selected by a later click, or reopen a review after the user presses Done, because every response unconditionally calls `setActiveReview`. Tracking a request generation and invalidating it from `hideReview` would ensure only the latest request can commit state.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts:15">
P2: Receipt IDs intended for app-only review controls are exposed to model-generated Code Mode code and can be echoed under another property, bypassing the metadata stripper. Keeping the receipt in a server-side/call side channel, or otherwise removing it before the sandbox sees the result, would preserve the app-only boundary.</violation>
</file>

<file name="src/features/workspaces/documents/use-document-edit-receipt-undo.ts">

<violation number="1" location="src/features/workspaces/documents/use-document-edit-receipt-undo.ts:24">
P2: A delayed undo response can close the wrong document's review. Make the hide conditional on the active review still matching this undo target (or use a review token) so switching receipts while the request is pending does not discard the new review.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@@ -0,0 +1,74 @@
import { createServerFn } from "@tanstack/react-start";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The PR introduces new server functions getDocumentEditReceiptReviewFn and undoDocumentEditReceiptFn that implement the core review/undo behavior for AI document edits, but no tests exercise these handlers. Adding regression-style tests for the validation schema, authorization branching (read vs mutate), and the delegation to the document session would be practical and would help prevent regressions in this new workflow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-edit-review-functions.ts, line 40:

<comment>The PR introduces new server functions `getDocumentEditReceiptReviewFn` and `undoDocumentEditReceiptFn` that implement the core review/undo behavior for AI document edits, but no tests exercise these handlers. Adding regression-style tests for the validation schema, authorization branching (`read` vs `mutate`), and the delegation to the document session would be practical and would help prevent regressions in this new workflow.</comment>

<file context>
@@ -0,0 +1,74 @@
+			: result;
+	});
+
+export const undoDocumentEditReceiptFn = createServerFn({ method: "POST" })
+	.validator(documentEditReceiptInputSchema)
+	.handler(async ({ data }): Promise<DocumentEditReceiptUndoResult> => {
</file context>

itemInput.type === "document" && itemInput.initialContent !== undefined
? {
...itemInput,
initialContent: await resolveDocumentCitations({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The behavioral change in how initial document content is processed — now flowing through resolveDocumentCitations and parseDocumentAiHtml instead of the old Markdown projection — is not exercised by any tests for this operation. I found no test files covering create-items.ts, createWorkspaceItemsOperation, resolveDocumentCitations, or initialContent. While the underlying parser has unit tests, the integration path in this file is entirely untested, and a regression-style assertion (e.g., creating an item with HTML content and asserting successful parsing and no warnings) is practical. Consider adding tests that exercise the new HTML/citation pipeline through createWorkspaceItemsOperation and assert the removal of warnings from the result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/operations/create-items.ts, line 125:

<comment>The behavioral change in how initial document content is processed — now flowing through `resolveDocumentCitations` and `parseDocumentAiHtml` instead of the old Markdown projection — is not exercised by any tests for this operation. I found no test files covering `create-items.ts`, `createWorkspaceItemsOperation`, `resolveDocumentCitations`, or `initialContent`. While the underlying parser has unit tests, the integration path in this file is entirely untested, and a regression-style assertion (e.g., creating an item with HTML content and asserting successful parsing and no warnings) is practical. Consider adding tests that exercise the new HTML/citation pipeline through `createWorkspaceItemsOperation` and assert the removal of `warnings` from the result.</comment>

<file context>
@@ -118,7 +118,18 @@ export async function createWorkspaceItemsOperation(
+			itemInput.type === "document" && itemInput.initialContent !== undefined
+				? {
+						...itemInput,
+						initialContent: await resolveDocumentCitations({
+							context: accessContext,
+							html: itemInput.initialContent,
</file context>

@@ -0,0 +1,204 @@
import { Extension, type Editor } from "@tiptap/core";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new review-mark derivation logic (ChangeSet diffing, custom TokenEncoder, simplifyChanges, and decoration creation for insertions, deletions, and atom blocks) is not accompanied by tests in this diff. Behavior-change/feature PRs should exercise changed behavior with assertions rather than leaving complex logic unverified. Consider adding unit tests that exercise createDocumentEditReviewDecorations with concrete before/after documents and assert the resulting DecorationSet for insertions, deletions, attribute-only changes, and atom-block changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-edit-review-extension.ts, line 111:

<comment>The new review-mark derivation logic (ChangeSet diffing, custom TokenEncoder, `simplifyChanges`, and decoration creation for insertions, deletions, and atom blocks) is not accompanied by tests in this diff. Behavior-change/feature PRs should exercise changed behavior with assertions rather than leaving complex logic unverified. Consider adding unit tests that exercise `createDocumentEditReviewDecorations` with concrete before/after documents and assert the resulting `DecorationSet` for insertions, deletions, attribute-only changes, and atom-block changes.</comment>

<file context>
@@ -0,0 +1,204 @@
+	);
+}
+
+function createDocumentEditReviewDecorations(
+	beforeDocument: TiptapDocumentJson,
+	afterDocument: ProseMirrorNode,
</file context>

this.activeWorkspaceReferences.push(...records);
}

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new _resolveWorkspaceReferences method combines transcript references with in-flight activeWorkspaceReferences to let tools resolve refs from the current turn before they hit the transcript. This is a non-trivial behavior change in a behavior-change PR, yet no visible tests exercise this path. A regression-style test (e.g., simulating a document read followed by a ref citation within the same turn) would be practical and should be added to verify the merging logic works correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/ai-thread.ts, line 521:

<comment>The new `_resolveWorkspaceReferences` method combines transcript references with in-flight `activeWorkspaceReferences` to let tools resolve refs from the current turn before they hit the transcript. This is a non-trivial behavior change in a behavior-change PR, yet no visible tests exercise this path. A regression-style test (e.g., simulating a document read followed by a ref citation within the same turn) would be practical and should be added to verify the merging logic works correctly.</comment>

<file context>
@@ -517,6 +518,21 @@ export function createAIThreadClass(getUserAIStore: () => typeof UserAIStore) {
 			this.activeWorkspaceReferences.push(...records);
 		}
 
+		/**
+		 * Looks up the refs a read handed the assistant, so a tool can turn one
+		 * into the location it stands for. Reads from this turn are not in the
</file context>

if (wasServerSynced === localDocumentReadyValue) {
if (
wasServerSynced === localDocumentReadyValue &&
session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The added empty-fragment guard changes when a collaboration session is marked ready, introducing a new edge case where a previously-synced but empty document never reaches ready until a server round-trip. A regression-style test is practical here (e.g. asserting that markReady is withheld when the IndexedDB marker is present but the Yjs XML fragment is empty), yet no tests cover this file or the readiness logic. Consider adding a test for this guard so future changes to the restore path have coverage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/use-document-collaboration-session.ts, line 209:

<comment>The added empty-fragment guard changes when a collaboration session is marked ready, introducing a new edge case where a previously-synced but empty document never reaches ready until a server round-trip. A regression-style test is practical here (e.g. asserting that `markReady` is withheld when the IndexedDB marker is present but the Yjs XML fragment is empty), yet no tests cover this file or the readiness logic. Consider adding a test for this guard so future changes to the restore path have coverage.</comment>

<file context>
@@ -203,7 +204,10 @@ function createActiveDocumentSession(input: {
-			if (wasServerSynced === localDocumentReadyValue) {
+			if (
+				wasServerSynced === localDocumentReadyValue &&
+				session.ydoc.getXmlFragment(tiptapDocumentYjsField).length > 0
+			) {
 				markReady();
</file context>

const { reveal } = useWorkspaceLocationActions();
const [activeReview, setActiveReview] = useState<ActiveDocumentEditReview | null>(null);
const hideReview = useCallback(() => setActiveReview(null), []);
const showReview = useCallback(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The showReview callback and related review-state logic in this new context file are not covered by any unit test. Because this is a behavior-change PR introducing a review/undo workflow, Rule 1 expects new behavior to be exercised by tests when practical. Consider adding tests that cover the async showReview flow (successful load, each unavailable status mapping to the correct toast message, and state transitions) as well as hideReview and the missing-provider error path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-edit-review-context.tsx, line 43:

<comment>The `showReview` callback and related review-state logic in this new context file are not covered by any unit test. Because this is a behavior-change PR introducing a review/undo workflow, Rule 1 expects new behavior to be exercised by tests when practical. Consider adding tests that cover the async `showReview` flow (successful load, each unavailable status mapping to the correct toast message, and state transitions) as well as `hideReview` and the missing-provider error path.</comment>

<file context>
@@ -0,0 +1,96 @@
+	const { reveal } = useWorkspaceLocationActions();
+	const [activeReview, setActiveReview] = useState<ActiveDocumentEditReview | null>(null);
+	const hideReview = useCallback(() => setActiveReview(null), []);
+	const showReview = useCallback(
+		async (input: { itemId: string; receiptIds: string[] }) => {
+			// reveal opens the document, or focuses the tab already holding it, and
</file context>

return {
...output,
[aiThreadToolUiMetadataKey]: {
documentEditReceiptId: receiptId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Receipt IDs intended for app-only review controls are exposed to model-generated Code Mode code and can be echoed under another property, bypassing the metadata stripper. Keeping the receipt in a server-side/call side channel, or otherwise removing it before the sandbox sees the result, would preserve the app-only boundary.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/ai-thread-tool-ui-metadata.ts, line 15:

<comment>Receipt IDs intended for app-only review controls are exposed to model-generated Code Mode code and can be echoed under another property, bypassing the metadata stripper. Keeping the receipt in a server-side/call side channel, or otherwise removing it before the sandbox sees the result, would preserve the app-only boundary.</comment>

<file context>
@@ -0,0 +1,52 @@
+	return {
+		...output,
+		[aiThreadToolUiMetadataKey]: {
+			documentEditReceiptId: receiptId,
+		} satisfies AIThreadToolUiMetadata,
+	};
</file context>

return;
}

setActiveReview({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A slower earlier review request can replace the review selected by a later click, or reopen a review after the user presses Done, because every response unconditionally calls setActiveReview. Tracking a request generation and invalidating it from hideReview would ensure only the latest request can commit state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-edit-review-context.tsx, line 65:

<comment>A slower earlier review request can replace the review selected by a later click, or reopen a review after the user presses Done, because every response unconditionally calls `setActiveReview`. Tracking a request generation and invalidating it from `hideReview` would ensure only the latest request can commit state.</comment>

<file context>
@@ -0,0 +1,96 @@
+				return;
+			}
+
+			setActiveReview({
+				beforeDocument: review.beforeDocument,
+				itemId: input.itemId,
</file context>

Comment thread src/features/workspaces/documents/use-document-edit-receipt-undo.ts Outdated

return {
beforeDocument,
decorations: createDocumentEditReviewDecorations(beforeDocument, newState.doc),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: While a review is active the document is read-only, but it is still collaborative — any remote teammate typing arrives as a docChanged transaction, and the plugin's apply recomputes a full-document ChangeSet diff (plus a per-change document.forEach in addChangedAtomDecorations) on every such transaction. For a large or actively-edited document this is a meaningful recompute repeated for the whole duration of the review. Consider caching/recomputing the decorations only when the review is shown (or at most debounced/throttled), since interacting with the review itself doesn't change the doc while it is read-only.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-edit-review-extension.ts, line 76:

<comment>While a review is active the document is read-only, but it is still collaborative — any remote teammate typing arrives as a `docChanged` transaction, and the plugin's `apply` recomputes a full-document `ChangeSet` diff (plus a per-change `document.forEach` in `addChangedAtomDecorations`) on every such transaction. For a large or actively-edited document this is a meaningful recompute repeated for the whole duration of the review. Consider caching/recomputing the decorations only when the review is shown (or at most debounced/throttled), since interacting with the review itself doesn't change the doc while it is read-only.</comment>

<file context>
@@ -0,0 +1,204 @@
+
+						return {
+							beforeDocument,
+							decorations: createDocumentEditReviewDecorations(beforeDocument, newState.doc),
+						};
+					},
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/WorkspaceLayout.tsx">

<violation number="1" location="src/features/workspaces/components/WorkspaceLayout.tsx:309">
P2: Switching between cached workspaces can retain the previous document's review state and resurrect its review when the user returns, because this stateful provider is not scoped to the current `workspaceId`. Remount the provider per workspace or clear `activeReview` whenever the workspace changes.</violation>
</file>

<file name="src/features/workspaces/ai/workspace-tools.ts">

<violation number="1" location="src/features/workspaces/ai/workspace-tools.ts:21">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `resolveWorkspaceReferences` capability is threaded through `createAIThreadWorkspaceTools` → `createWorkspaceThreadTool` → `createThreadWorkspaceAccessContext`, but no tests exercise this new path. The PR introduces a new reference-resolution behavior that should be validated by tests — for example, verifying that the callback is forwarded to `createWorkspaceAccessContext` when provided and omitted when absent. Adding coverage for this path would protect against future regressions where the wiring is accidentally dropped.</violation>
</file>

<file name="src/features/workspaces/model/workspace-ai-context-prompt.ts">

<violation number="1" location="src/features/workspaces/model/workspace-ai-context-prompt.ts:78">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new empty-workspace prompt path is not covered by tests. Under the "Flag AI Slop and Fabricated Changes" rule, behavior-change PRs should exercise the changed behavior when a practical regression assertion exists. Add a test that passes a snapshot with `outline.totalItems === 0` and asserts the prompt includes the "this workspace is empty" message.</violation>
</file>

<file name="src/features/workspaces/operations/document-citations.ts">

<violation number="1" location="src/features/workspaces/operations/document-citations.ts:17">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `resolveDocumentCitations` function introduces document-level citation resolution behavior (ref→label mapping, PDF page-number formatting, and HTML re-injection), but no tests in the diff or repository exercise it. Rule 1 flags behavior-change code that lacks tests when regression-style assertions are practical, which is the case here: mocked `resolveWorkspaceReferences` and `getItemPaths` inputs would readily let you assert that a `wr_` ref maps to `"Document Name, p. 3"` for a PDF page, or plain `"Document Name"` otherwise. Adding unit tests for this path would protect against silent regressions in citation formatting and unknown-item filtering.</violation>
</file>

<file name="src/features/workspaces/documents/document-edit-review-context.tsx">

<violation number="1" location="src/features/workspaces/documents/document-edit-review-context.tsx:65">
P2: A slower review request can overwrite a newer document selection, leaving the wrong document's changes active when users click multiple receipt rows before the first request finishes. Guard the response with a request-generation/token check (and invalidate it when hiding or changing the selection) before calling `setActiveReview`.</violation>
</file>

<file name="src/features/workspaces/documents/document-ai-edits.ts">

<violation number="1" location="src/features/workspaces/documents/document-ai-edits.ts:181">
P2: Receipt line tallies overcount unchanged horizontal rules and block-math blocks after a whole-document rewrite. Exclude the top-level AI ref from the atom JSON before using it as the line key.</violation>
</file>

<file name="src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx">

<violation number="1" location="src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx:60">
P2: Reviewing a document fails after more than eight edits in one AI turn because this sends the entire unbounded receipt list while older receipts have already been evicted. The receipt group needs a retention/size guarantee shared with the server, or the UI needs to avoid presenting a review action that cannot be resolved.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

definition: (typeof workspaceToolDefinitions)[number];
getThreadContext: () => Promise<AIThreadContext | null>;
onWorkspaceReferences?: (records: readonly WorkspaceReferenceRecord[]) => void;
resolveWorkspaceReferences?: (refs: readonly string[]) => Promise<WorkspaceReferenceRecord[]>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new resolveWorkspaceReferences capability is threaded through createAIThreadWorkspaceToolscreateWorkspaceThreadToolcreateThreadWorkspaceAccessContext, but no tests exercise this new path. The PR introduces a new reference-resolution behavior that should be validated by tests — for example, verifying that the callback is forwarded to createWorkspaceAccessContext when provided and omitted when absent. Adding coverage for this path would protect against future regressions where the wiring is accidentally dropped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/workspace-tools.ts, line 21:

<comment>The new `resolveWorkspaceReferences` capability is threaded through `createAIThreadWorkspaceTools` → `createWorkspaceThreadTool` → `createThreadWorkspaceAccessContext`, but no tests exercise this new path. The PR introduces a new reference-resolution behavior that should be validated by tests — for example, verifying that the callback is forwarded to `createWorkspaceAccessContext` when provided and omitted when absent. Adding coverage for this path would protect against future regressions where the wiring is accidentally dropped.</comment>

<file context>
@@ -18,6 +18,7 @@ type WorkspaceThreadToolConfig = {
 	definition: (typeof workspaceToolDefinitions)[number];
 	getThreadContext: () => Promise<AIThreadContext | null>;
 	onWorkspaceReferences?: (records: readonly WorkspaceReferenceRecord[]) => void;
+	resolveWorkspaceReferences?: (refs: readonly string[]) => Promise<WorkspaceReferenceRecord[]>;
 };
 
</file context>

function formatWorkspaceAiContextOutline(outline: WorkspaceAiContextOutline) {
// Said plainly. Reporting "0 items complete" buries an empty workspace in a
// sentence whose grammar reads as a truncation notice.
if (outline.totalItems === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new empty-workspace prompt path is not covered by tests. Under the "Flag AI Slop and Fabricated Changes" rule, behavior-change PRs should exercise the changed behavior when a practical regression assertion exists. Add a test that passes a snapshot with outline.totalItems === 0 and asserts the prompt includes the "this workspace is empty" message.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/model/workspace-ai-context-prompt.ts, line 78:

<comment>The new empty-workspace prompt path is not covered by tests. Under the "Flag AI Slop and Fabricated Changes" rule, behavior-change PRs should exercise the changed behavior when a practical regression assertion exists. Add a test that passes a snapshot with `outline.totalItems === 0` and asserts the prompt includes the "this workspace is empty" message.</comment>

<file context>
@@ -73,6 +73,12 @@ export function formatWorkspaceAiContextForPrompt(value: unknown) {
 function formatWorkspaceAiContextOutline(outline: WorkspaceAiContextOutline) {
+	// Said plainly. Reporting "0 items complete" buries an empty workspace in a
+	// sentence whose grammar reads as a truncation notice.
+	if (outline.totalItems === 0) {
+		return ["- Workspace outline: this workspace is empty. It has no items yet."];
+	}
</file context>

* lets the document store the item and page it points at; what that source is
* called is read from the workspace when the citation is drawn.
*/
export async function resolveDocumentCitations(input: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new resolveDocumentCitations function introduces document-level citation resolution behavior (ref→label mapping, PDF page-number formatting, and HTML re-injection), but no tests in the diff or repository exercise it. Rule 1 flags behavior-change code that lacks tests when regression-style assertions are practical, which is the case here: mocked resolveWorkspaceReferences and getItemPaths inputs would readily let you assert that a wr_ ref maps to "Document Name, p. 3" for a PDF page, or plain "Document Name" otherwise. Adding unit tests for this path would protect against silent regressions in citation formatting and unknown-item filtering.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/operations/document-citations.ts, line 17:

<comment>The new `resolveDocumentCitations` function introduces document-level citation resolution behavior (ref→label mapping, PDF page-number formatting, and HTML re-injection), but no tests in the diff or repository exercise it. Rule 1 flags behavior-change code that lacks tests when regression-style assertions are practical, which is the case here: mocked `resolveWorkspaceReferences` and `getItemPaths` inputs would readily let you assert that a `wr_` ref maps to `"Document Name, p. 3"` for a PDF page, or plain `"Document Name"` otherwise. Adding unit tests for this path would protect against silent regressions in citation formatting and unknown-item filtering.</comment>

<file context>
@@ -0,0 +1,60 @@
+ * lets the document store the item and page it points at, named as the reader
+ * knows it.
+ */
+export async function resolveDocumentCitations(input: {
+	context: WorkspaceAccessContext;
+	html: string;
</file context>

) : (
workspaceInteractionContent
)}
<DocumentEditReviewProvider workspaceId={workspace.id}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Switching between cached workspaces can retain the previous document's review state and resurrect its review when the user returns, because this stateful provider is not scoped to the current workspaceId. Remount the provider per workspace or clear activeReview whenever the workspace changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/WorkspaceLayout.tsx, line 309:

<comment>Switching between cached workspaces can retain the previous document's review state and resurrect its review when the user returns, because this stateful provider is not scoped to the current `workspaceId`. Remount the provider per workspace or clear `activeReview` whenever the workspace changes.</comment>

<file context>
@@ -305,11 +306,13 @@ export function WorkspaceShell({
-				) : (
-					workspaceInteractionContent
-				)}
+				<DocumentEditReviewProvider workspaceId={workspace.id}>
+					{hasHeavyViewerRuntimeItems ? (
+						<WorkspacePdfEngineProvider>{workspaceInteractionContent}</WorkspacePdfEngineProvider>
</file context>
Suggested change
<DocumentEditReviewProvider workspaceId={workspace.id}>
<DocumentEditReviewProvider key={workspace.id} workspaceId={workspace.id}>

type="button"
className="flex w-full min-w-0 items-center gap-2 px-2.5 py-2 text-left transition-colors hover:bg-foreground/5"
onClick={() => {
void showReview({ itemId: group.itemId, receiptIds: group.receiptIds });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Reviewing a document fails after more than eight edits in one AI turn because this sends the entire unbounded receipt list while older receipts have already been evicted. The receipt group needs a retention/size guarantee shared with the server, or the UI needs to avoid presenting a review action that cannot be resolved.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/AiChatDocumentEditActions.tsx, line 60:

<comment>Reviewing a document fails after more than eight edits in one AI turn because this sends the entire unbounded receipt list while older receipts have already been evicted. The receipt group needs a retention/size guarantee shared with the server, or the UI needs to avoid presenting a review action that cannot be resolved.</comment>

<file context>
@@ -0,0 +1,104 @@
+			type="button"
+			className="flex w-full min-w-0 items-center gap-2 px-2.5 py-2 text-left transition-colors hover:bg-foreground/5"
+			onClick={() => {
+				void showReview({ itemId: group.itemId, receiptIds: group.receiptIds });
+			}}
+		>
</file context>

return;
}

setActiveReview({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A slower review request can overwrite a newer document selection, leaving the wrong document's changes active when users click multiple receipt rows before the first request finishes. Guard the response with a request-generation/token check (and invalidate it when hiding or changing the selection) before calling setActiveReview.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-edit-review-context.tsx, line 65:

<comment>A slower review request can overwrite a newer document selection, leaving the wrong document's changes active when users click multiple receipt rows before the first request finishes. Guard the response with a request-generation/token check (and invalidate it when hiding or changing the selection) before calling `setActiveReview`.</comment>

<file context>
@@ -0,0 +1,96 @@
+				return;
+			}
+
+			setActiveReview({
+				beforeDocument: review.beforeDocument,
+				itemId: input.itemId,
</file context>

Comment thread src/features/workspaces/documents/use-document-edit-receipt-undo.ts Outdated
Comment thread src/features/workspaces/documents/use-document-edit-receipt-undo.ts Outdated
}
// A rule or a formula holds no text but still occupies a line.
if (node.isAtom) {
countLine(JSON.stringify(node.toJSON()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Receipt line tallies overcount unchanged horizontal rules and block-math blocks after a whole-document rewrite. Exclude the top-level AI ref from the atom JSON before using it as the line key.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-ai-edits.ts, line 181:

<comment>Receipt line tallies overcount unchanged horizontal rules and block-math blocks after a whole-document rewrite. Exclude the top-level AI ref from the atom JSON before using it as the line key.</comment>

<file context>
@@ -0,0 +1,312 @@
+			}
+			// A rule or a formula holds no text but still occupies a line.
+			if (node.isAtom) {
+				countLine(JSON.stringify(node.toJSON()));
+				return false;
+			}
</file context>

@coderabbitai coderabbitai Bot mentioned this pull request Aug 1, 2026
The undo hook existed to be shared between the chat receipt and the
review toolbar. The receipt's undo button is gone, so it had one caller
and a comment describing a sharing that no longer happens. Fold it into
that caller, and inline the two-branch authorization helper alongside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/workspaces/documents/tiptap-schema.ts (1)

54-87: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Confirm that nested list/table blocks do not receive top-level fingerprints.

DocumentAiRef attributes are attached by group.includes("block"), so any list/tree/table block group members registered through StarterKit, TaskList, or TableKit can inherit aiRef. This can expose nested block content as targetable data-ref="..." in serializeTiptapDocumentToAiHtml; either remove doc/block/list/table groups from fingerprint eligibility or apply fingerprints only to top-level doc children. Discarded data-ref values are already stripped before parsing AI HTML, and applyDocumentAiEdits recomputes targetRefByStableRef before resolving requests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/documents/tiptap-schema.ts` around lines 54 - 87,
Restrict DocumentAiRef in addGlobalAttributes so aiRef is applied only to
top-level document children, excluding nested list, tree, and table block nodes
contributed by StarterKit, TaskList, or TableKit. Preserve the existing
parseHTML sanitization and renderHTML behavior, and ensure
serializeTiptapDocumentToAiHtml cannot emit fingerprints for nested blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/features/workspaces/documents/tiptap-schema.ts`:
- Around line 54-87: Restrict DocumentAiRef in addGlobalAttributes so aiRef is
applied only to top-level document children, excluding nested list, tree, and
table block nodes contributed by StarterKit, TaskList, or TableKit. Preserve the
existing parseHTML sanitization and renderHTML behavior, and ensure
serializeTiptapDocumentToAiHtml cannot emit fingerprints for nested blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bfaa14a2-fef6-495a-9468-ac60edbf646c

📥 Commits

Reviewing files that changed from the base of the PR and between 0b0820a and 7e578b9.

📒 Files selected for processing (11)
  • src/features/workspaces/components/document-editor/DocumentEditUndoButton.tsx
  • src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx
  • src/features/workspaces/documents/document-citation-node.tsx
  • src/features/workspaces/documents/document-edit-receipt.ts
  • src/features/workspaces/documents/document-edit-review-functions.ts
  • src/features/workspaces/documents/tiptap-extensions.ts
  • src/features/workspaces/documents/tiptap-schema.ts
  • src/features/workspaces/operations/document-citations.ts
  • src/features/workspaces/operations/edit-item.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
  • src/styles.css
💤 Files with no reviewable changes (3)
  • src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx
  • src/styles.css
  • src/features/workspaces/documents/document-edit-receipt.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/features/workspaces/operations/edit-item.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
  • src/features/workspaces/documents/document-edit-review-functions.ts
  • src/features/workspaces/operations/document-citations.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 12 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/features/workspaces/components/document-editor/DocumentEditUndoButton.tsx Outdated
urjitc and others added 4 commits August 2, 2026 17:19
The worker project could not reach a binding. Importing `cloudflare:test`
made the pool build the app entry, which pulls the TanStack Start handler
and its build-time virtual specifiers, and those only resolve under the app
build. So the whole stateful layer - kernel, document sessions, AI store,
and every purge, alarm and schedule path - had no coverage at all, and the
one class of bug that only surfaces there was untestable by construction.

Four things were in the way. Every durable object transitively imports
`@tanstack/react-start/server` for one ambient-header fallback, so tests
alias it to a stub that throws: a test that reaches the ambient request
context has found a path which cannot run inside a durable object, and
should say so rather than pass on blank headers. Tests run against a
handler-free entry instead of the app's. Agent classes use TC39 decorators,
which Oxc cannot lower yet, so the project borrows the same `agents/vite`
plugin the app build already uses, and the transform now matches what
ships. The export list moves to one module both entries re-export, so a new
durable object cannot reach production while staying invisible here.

The suite also never ran: `test:workers` was defined and referenced
nowhere, and CI runs `ciTest`, which was the node config. Folding both into
one config as projects means `vp test` runs everything, so there is no
second suite left to forget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`deleteAll()` empties storage but does not evict the durable object, and
the constructor that created the schema has already run. The instance stays
resident over a database with no tables, so everything still in flight
queries tables that are gone until the runtime happens to evict it.

Production showed exactly that. Two seconds after a workspace purge
succeeded, seven in-flight extraction workflows hit the wiped object and
failed with `no such table: kernel_items`, twice over, until an eviction
about twenty seconds later reconstructed it - after which the same calls
failed with `Workspace item not found.` instead. The error changing mid
retry is what pins the cause to residency rather than to the deletion.

Guard the single closure every query already routes through, rather than
each method, so a path added later cannot forget it. This does not stop the
callers failing - the workspaces really are deleted - but it replaces an
undefined state with a defined one, and a SQL error nobody can act on with
a sentence that says what happened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Storage writes commit with whichever invocation caused the object to be
constructed. When that invocation is canceled the writes roll back, but the
instance stays in memory with its constructor already finished, so the
schema it believes it created is not there and never will be until an
eviction.

This is the same missing-table failure as a purge, reached from the other
direction, and production has one: a `setName` invocation recorded as
canceled, an item creation on that instance failing with `no such table:
kernel_items`, and an identical creation succeeding 279ms later on a fresh
one.

`blockConcurrencyWhile()` is what Cloudflare's own guidance prescribes for
schema setup, and it holds the writes inside a gate no request can be
delivered during. The agents base class builds its own schema synchronously
and calls `onStart` from `setName`, so the gate still runs first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Account deletion has the same shape as the workspace kernel's purge:
`deleteAll()` empties storage, the instance stays resident, and its schema
is gone. Nothing guarded it, so anything reaching the store between the
purge and an eviction reads tables that no longer exist.

Three durable objects now run this same lifecycle and each had answered it
differently - document sessions already track a deleted flag, the kernel
grew one, and this had nothing. It has not shown up in error tracking yet
only because account deletion is far rarer than workspace deletion.

Overriding `sql` covers every read and write in one place, because the
directory-store helpers all reach storage through the inherited method.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/workspaces/kernel/workspace-kernel.ts (1)

79-95: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Route ShellWorkspace storage through kernelSql too.

this.workspace is built with sql: this.ctx.storage.sql, while purged only guards kernelSql. WorkspaceKernelItemCommands receives that workspace reference and can call readFile, writeFile, mkdir, and rm after the Durable Object schema has been deleted. These SQL-backed filesystem operations then bypass the "Workspace deleted." guard; pass kernelSql when constructing ShellWorkspace instead of the raw ctx.storage.sql.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/kernel/workspace-kernel.ts` around lines 79 - 95,
Update the ShellWorkspace construction in the workspace field initializer to
pass the existing kernelSql handler instead of this.ctx.storage.sql, ensuring
readFile, writeFile, mkdir, and rm operations honor the purged guard and throw
"Workspace deleted." after schema deletion.
🧹 Nitpick comments (1)
src/features/workspaces/ai/user-ai-store-purge.worker.test.ts (1)

15-33: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add an eviction-and-rebuild test to match workspace-kernel-purge.worker.test.ts coverage.

This file tests only the live-instance rejection path. workspace-kernel-purge.worker.test.ts also tests that a purged-then-evicted instance rebuilds an empty schema. Since UserAIStore initializes its schema in onStart() instead of the constructor, this is a different lifecycle code path and deserves its own eviction-and-rebuild test to confirm the schema rebuild works after eviction.

🧪 Proposed additional test
+	it("rebuilds an empty schema once the purged instance is evicted", async () => {
+		const namespace = Reflect.get(env as object, "UserAIStore") as DurableObjectNamespace;
+		const userId = "purge-after-eviction";
+
+		const agent = (await getAgentByName(namespace as never, userId)) as unknown as TestUserAIStore;
+		await agent.purgeForDeletion();
+
+		const stub = namespace.get(namespace.idFromName(userId));
+		await evictDurableObject(stub);
+
+		const rebuilt = (await getAgentByName(namespace as never, userId)) as unknown as TestUserAIStore;
+		await expect(rebuilt.purgeForDeletion()).resolves.toMatchObject({ failed: 0 });
+	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/ai/user-ai-store-purge.worker.test.ts` around lines
15 - 33, Add a second test in the “user AI store purge” suite covering purge
followed by instance eviction and recreation, matching the eviction-and-rebuild
scenario from workspace-kernel-purge.worker.test.ts. Use the existing
UserAIStore agent/bootstrap path so onStart() runs, then verify the recreated
instance rebuilds an empty schema and supports the expected post-purge behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/features/workspaces/kernel/workspace-kernel.ts`:
- Around line 79-95: Update the ShellWorkspace construction in the workspace
field initializer to pass the existing kernelSql handler instead of
this.ctx.storage.sql, ensuring readFile, writeFile, mkdir, and rm operations
honor the purged guard and throw "Workspace deleted." after schema deletion.

---

Nitpick comments:
In `@src/features/workspaces/ai/user-ai-store-purge.worker.test.ts`:
- Around line 15-33: Add a second test in the “user AI store purge” suite
covering purge followed by instance eviction and recreation, matching the
eviction-and-rebuild scenario from workspace-kernel-purge.worker.test.ts. Use
the existing UserAIStore agent/bootstrap path so onStart() runs, then verify the
recreated instance rebuilds an empty schema and supports the expected post-purge
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b82d9d0a-9fed-4997-9f86-7d049744181d

📥 Commits

Reviewing files that changed from the base of the PR and between 7e578b9 and c06f8b0.

📒 Files selected for processing (13)
  • knip.json
  • package.json
  • src/durable-objects.ts
  • src/features/workspaces/ai/user-ai-agents.ts
  • src/features/workspaces/ai/user-ai-store-purge.worker.test.ts
  • src/features/workspaces/kernel/workspace-kernel-purge.worker.test.ts
  • src/features/workspaces/kernel/workspace-kernel.ts
  • src/server.ts
  • test/stubs/tanstack-start-server.ts
  • test/worker-entry.ts
  • tsconfig.json
  • vitest.cloudflare.config.ts
  • vitest.config.ts
💤 Files with no reviewable changes (2)
  • vitest.cloudflare.config.ts
  • knip.json

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/features/workspaces/kernel/workspace-kernel.ts
Undo returned "reverted" both after reverting and when it found the
edits already reverted, so clicking it a second time claimed to have
undone changes it had not touched. Name the outcome that changed the
document, and the already-undone case falls through to the message that
was sitting unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 171 unresolved issues from previous reviews.

Re-trigger cubic

The guard only covers kernelSql. ShellWorkspace keeps its own handle on
ctx.storage.sql, so "every query" was never true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
}) {
const [isConfirming, setIsConfirming] = useState(false);
const { hideReview } = useDocumentEditReview();
const undoMutation = useMutation({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/query-mutation-missing-invalidation (warning)

useMutation with no cache update here can leave your users looking at stale data after it runs.

Fix → Add onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] }) so cached data stays in sync after the mutation

Docs

name: "documentAiRef",

addGlobalAttributes() {
const blockTypes = this.extensions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/js-combine-iterations (warning)

This loops over your list twice because .filter().map() makes two passes, so do it in one pass with .reduce() or a for...of loop

Fix → Combine .map().filter() style chains into one pass with .reduce() or a for...of loop, so you only loop over the list once

Docs

(extension) =>
extension.type === "node" &&
typeof extension.config?.group === "string" &&
extension.config.group.split(/\s+/).includes("block"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/js-set-map-lookups (warning)

This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.

Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time

Docs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 170 unresolved issues from previous reviews.

Re-trigger cubic

@urjitc
urjitc merged commit da3041e into main Aug 2, 2026
12 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant